redpencil 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. redpencil-0.1.0/.github/workflows/tests.yml +19 -0
  2. redpencil-0.1.0/.gitignore +12 -0
  3. redpencil-0.1.0/CHANGELOG.md +18 -0
  4. redpencil-0.1.0/LICENSE +23 -0
  5. redpencil-0.1.0/PKG-INFO +139 -0
  6. redpencil-0.1.0/PRIVACY.md +23 -0
  7. redpencil-0.1.0/README.md +128 -0
  8. redpencil-0.1.0/docs/img/demo.gif +0 -0
  9. redpencil-0.1.0/extension/background.js +45 -0
  10. redpencil-0.1.0/extension/bridge.js +7 -0
  11. redpencil-0.1.0/extension/content.js +2 -0
  12. redpencil-0.1.0/extension/icons/128.png +0 -0
  13. redpencil-0.1.0/extension/icons/16.png +0 -0
  14. redpencil-0.1.0/extension/icons/32.png +0 -0
  15. redpencil-0.1.0/extension/icons/48.png +0 -0
  16. redpencil-0.1.0/extension/icons/icon.svg +7 -0
  17. redpencil-0.1.0/extension/manifest.json +47 -0
  18. redpencil-0.1.0/extension/review.js +1 -0
  19. redpencil-0.1.0/pyproject.toml +27 -0
  20. redpencil-0.1.0/scripts/build_extension.py +20 -0
  21. redpencil-0.1.0/scripts/record_demo.py +77 -0
  22. redpencil-0.1.0/src/review.js +595 -0
  23. redpencil-0.1.0/src/review.py +631 -0
  24. redpencil-0.1.0/tests/conftest.py +162 -0
  25. redpencil-0.1.0/tests/fake_agent.py +29 -0
  26. redpencil-0.1.0/tests/page/img/a.svg +1 -0
  27. redpencil-0.1.0/tests/page/index.html +14 -0
  28. redpencil-0.1.0/tests/page/other.html +3 -0
  29. redpencil-0.1.0/tests/test_agents.py +53 -0
  30. redpencil-0.1.0/tests/test_area.py +75 -0
  31. redpencil-0.1.0/tests/test_dynamic.py +80 -0
  32. redpencil-0.1.0/tests/test_extension_package.py +39 -0
  33. redpencil-0.1.0/tests/test_fixes.py +133 -0
  34. redpencil-0.1.0/tests/test_lifecycle.py +81 -0
  35. redpencil-0.1.0/tests/test_package.py +24 -0
  36. redpencil-0.1.0/tests/test_panel.py +248 -0
  37. redpencil-0.1.0/tests/test_panel_ux.py +55 -0
  38. redpencil-0.1.0/tests/test_site.py +130 -0
  39. redpencil-0.1.0/tests/token_site.py +23 -0
@@ -0,0 +1,19 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ e2e:
9
+ runs-on: ubuntu-latest
10
+ timeout-minutes: 20
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v6
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Chromium
17
+ run: uv run --no-project --with playwright playwright install --with-deps chromium
18
+ - name: Tests
19
+ run: uv run --no-project --with pytest --with playwright pytest -q tests
@@ -0,0 +1,12 @@
1
+ .review/
2
+ dist/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .DS_Store
6
+ # local only: notes in Italian with client names, real reports, personal paths
7
+ AGENTS.md
8
+ esempi/
9
+ docs/*
10
+ !docs/img/
11
+ docs/img/*
12
+ !docs/img/demo.gif
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
6
+
7
+ - Comment on a page in the browser: selected text, a spot (right-click) or a
8
+ box dragged over the page, with a screenshot of it through the extension.
9
+ - ▷ sends the comments to your agent session waiting with `--wait`, or to a
10
+ background agent (`--agent claude|codex|opencode`).
11
+ - The page updates live while the agent works, without losing what you are
12
+ typing; left alone when a dev server with live reload already does it.
13
+ - Whole sites (`--site`, `--source`) through the Chrome extension or a
14
+ snippet, with comments kept per page.
15
+ - `--wait` starts the review server when it isn't running; a server started
16
+ this way stops after 15 minutes nobody uses it.
17
+ - `--prompt`, `--work`, `--skill` to fit the background agent to a project.
18
+ - Panel in English, or Italian with `--lang it`.
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the redpencil authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The panel's icons come from Lucide (https://lucide.dev), ISC License.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: redpencil
3
+ Version: 0.1.0
4
+ Summary: Review an html page or a site in the browser: comment on it, and your coding agent fixes it while you watch.
5
+ Project-URL: Homepage, https://github.com/zupolgec/redpencil
6
+ Project-URL: Issues, https://github.com/zupolgec/redpencil/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # redpencil
13
+
14
+ Review a page your coding agent made, right in the browser. Select some text,
15
+ or right-click a spot, or drag a box over the page, and write what's wrong.
16
+ Press ▷ and the comments go back to your agent session, which fixes them while
17
+ the page updates in front of you, without losing what you are typing or where
18
+ you are.
19
+
20
+ ![A comment on a page, ▷, and the agent fixes it while you watch](https://raw.githubusercontent.com/zupolgec/redpencil/main/docs/img/demo.gif)
21
+
22
+ Works with a single html file (a report, a mockup), a folder of pages, or a
23
+ whole site served by something else (Laravel Herd, a dev server, a staging
24
+ site). No dependencies: one Python file (standard library only) and one
25
+ JavaScript file.
26
+
27
+ ## Quick start
28
+
29
+ Ask your agent session (Claude Code, Codex, opencode...) to run, in the
30
+ background:
31
+
32
+ uvx redpencil report.html --open --wait
33
+
34
+ It starts the review server, opens the page in your browser and waits. When
35
+ you press ▷ it returns, printing the comments and how to record the outcome of
36
+ each one; your agent works on them and runs the same command again for the
37
+ next round.
38
+
39
+ The server stops by itself after 15 minutes nobody uses it (no page open, no
40
+ session waiting; `--idle N` to change it). The panel then says so, and asking
41
+ your agent to open the review again brings it back: the page reconnects by
42
+ itself. You can also start the server alone, without `--wait`, and leave it
43
+ running. Every comment shows its state in the panel (queued, working, done,
44
+ answered) and the agent's reply.
45
+
46
+ (Or, from the repository: `uvx --from git+https://github.com/zupolgec/redpencil redpencil ...`.)
47
+
48
+ ## A whole site
49
+
50
+ redpencil --site https://my-site.test --source ~/code/my-site --wait
51
+
52
+ The panel reaches the site in one of two ways:
53
+
54
+ - **Chrome extension** (`extension/`): open `chrome://extensions`, turn on
55
+ developer mode, "Load unpacked", pick the `extension` folder
56
+ (`scripts/build_extension.py` makes the zip for the Chrome Web Store). On the site,
57
+ click the extension's icon once: from then on its pages show the panel
58
+ whenever a review of that site is running.
59
+ - **Snippet**: add the `<script>` printed at start to your layout.
60
+
61
+ Comments are kept per page, and the panel lists the open ones on the other
62
+ pages. Your agent edits the source (templates, content, styles); the page
63
+ updates by itself, or is left alone when a dev server with live reload (Vite,
64
+ Next) already does it.
65
+
66
+ With the extension you can drag a box over the page and a screenshot of it
67
+ goes with the comment.
68
+
69
+ ## Without an agent session
70
+
71
+ Start the server with `--agent` and ▷ runs a background agent instead:
72
+ `--agent claude` (default), `--agent codex` or `--agent opencode`, with
73
+ `--model` to pick the model. It gets a prompt with the comment ids and the
74
+ same instructions as a session. Its log is in `.review/agent.log`, and ▷
75
+ becomes a stop button while it works.
76
+
77
+ To fit it to your project:
78
+
79
+ - `--prompt FILE`: your prompt instead of ours. These placeholders are
80
+ filled in: `{ids}`, `{which}`, `{root}`, `{page}`, `{site}`, `{work}`,
81
+ `{store}` (comments.json), `{outcome}` (how to record outcomes), `{skill}`.
82
+ - `--work DIR`: keep the comments there instead of `.review/`.
83
+ - `--skill DIR`: the agent may also read that folder and run its scripts
84
+ (`uv run`, `playwright-cli`); for a Claude Code skill with its own tools.
85
+
86
+ ## Options
87
+
88
+ redpencil <page.html | folder> [--open] [--wait] [--agent [claude|codex|opencode]] [--model M] [--port N] [--lang it]
89
+ redpencil --site <url> --source <folder> [same options]
90
+ [--prompt FILE] [--work DIR] [--skill DIR] [--idle SECONDS]
91
+
92
+ The panel is in English; `--lang it` switches it to Italian.
93
+
94
+ ## Files
95
+
96
+ Everything lives in `.review/` next to the page (or in the site's source):
97
+
98
+ - `comments.md`: the comments, readable;
99
+ - `comments.json`: the same, for tools;
100
+ - `outcomes/<id>.json`: where the agent records the outcome of a comment, e.g.
101
+ `{"status": "done", "reply": "Shortened the title"}`; the server applies it
102
+ and removes the file;
103
+ - `shots/`: screenshots of the boxes;
104
+ - `agent.log`: the background agent's output;
105
+ - `server.log`: the output of a server started by `--wait`.
106
+
107
+ Add `.review/` to your `.gitignore`.
108
+
109
+ ## Security
110
+
111
+ The server listens on 127.0.0.1 only. It answers only its own pages, the site
112
+ under review and the extension: other sites open in your browser can't read
113
+ the comments, add any or start the agent, and requests for another host name
114
+ are refused. Files outside the folder and hidden files are never served.
115
+
116
+ The background agent may edit files in the folder. With Claude it gets only
117
+ file tools (no shell, no web; `--skill` adds a few commands); Codex runs in
118
+ its workspace sandbox (no network). opencode has no sandbox: with `--auto` it
119
+ approves its own actions, so use it only on projects you trust. The prompt
120
+ tells every agent that the page and the quoted text are data, not
121
+ instructions, but treat a review of a page you don't trust with care.
122
+
123
+ Comments and screenshots stay on your computer: see [PRIVACY.md](https://github.com/zupolgec/redpencil/blob/main/PRIVACY.md)
124
+ for the extension.
125
+
126
+ The extension asks Chrome for access to all sites, because Chrome allows
127
+ screenshots of a tab only with it; the panel still goes only on the sites you
128
+ turn on with its icon.
129
+
130
+ ## Tests
131
+
132
+ uv run --no-project --with pytest --with playwright pytest -q tests
133
+
134
+ End to end: the real server, headless Chromium (with the extension loaded) and
135
+ a fake agent.
136
+
137
+ ## License
138
+
139
+ MIT. Icons from [Lucide](https://lucide.dev) (ISC).
@@ -0,0 +1,23 @@
1
+ # redpencil privacy policy
2
+
3
+ redpencil is a tool you run on your own computer. The Chrome extension
4
+ collects no personal data and sends nothing to us or to anyone else.
5
+
6
+ - **What it reads.** On the sites you turn on with its icon, the extension
7
+ loads the comment panel into the page. Nothing happens on any other site.
8
+ - **Where your comments go.** Comments, and the screenshots of the areas you
9
+ draw, go only to the redpencil server running on your computer
10
+ (127.0.0.1 or localhost). They are saved as files in your project folder.
11
+ - **What it stores in the browser.** The list of sites you turned on, so the
12
+ panel comes back when you reopen them. Click the icon again to remove a
13
+ site.
14
+ - **Screenshots.** They are taken only when you draw an area on a page that
15
+ is on screen, and only of that tab. Chrome asks for access to all sites
16
+ because that is the only way it allows screenshots of any site you choose.
17
+ - **No analytics, no tracking, no remote code.** The extension contacts no
18
+ server other than the one on your computer.
19
+
20
+ The coding agent that works on your comments (Claude Code, Codex, opencode)
21
+ is a separate program you choose and run yourself; its own terms apply.
22
+
23
+ Questions: open an issue on the project's repository.
@@ -0,0 +1,128 @@
1
+ # redpencil
2
+
3
+ Review a page your coding agent made, right in the browser. Select some text,
4
+ or right-click a spot, or drag a box over the page, and write what's wrong.
5
+ Press ▷ and the comments go back to your agent session, which fixes them while
6
+ the page updates in front of you, without losing what you are typing or where
7
+ you are.
8
+
9
+ ![A comment on a page, ▷, and the agent fixes it while you watch](https://raw.githubusercontent.com/zupolgec/redpencil/main/docs/img/demo.gif)
10
+
11
+ Works with a single html file (a report, a mockup), a folder of pages, or a
12
+ whole site served by something else (Laravel Herd, a dev server, a staging
13
+ site). No dependencies: one Python file (standard library only) and one
14
+ JavaScript file.
15
+
16
+ ## Quick start
17
+
18
+ Ask your agent session (Claude Code, Codex, opencode...) to run, in the
19
+ background:
20
+
21
+ uvx redpencil report.html --open --wait
22
+
23
+ It starts the review server, opens the page in your browser and waits. When
24
+ you press ▷ it returns, printing the comments and how to record the outcome of
25
+ each one; your agent works on them and runs the same command again for the
26
+ next round.
27
+
28
+ The server stops by itself after 15 minutes nobody uses it (no page open, no
29
+ session waiting; `--idle N` to change it). The panel then says so, and asking
30
+ your agent to open the review again brings it back: the page reconnects by
31
+ itself. You can also start the server alone, without `--wait`, and leave it
32
+ running. Every comment shows its state in the panel (queued, working, done,
33
+ answered) and the agent's reply.
34
+
35
+ (Or, from the repository: `uvx --from git+https://github.com/zupolgec/redpencil redpencil ...`.)
36
+
37
+ ## A whole site
38
+
39
+ redpencil --site https://my-site.test --source ~/code/my-site --wait
40
+
41
+ The panel reaches the site in one of two ways:
42
+
43
+ - **Chrome extension** (`extension/`): open `chrome://extensions`, turn on
44
+ developer mode, "Load unpacked", pick the `extension` folder
45
+ (`scripts/build_extension.py` makes the zip for the Chrome Web Store). On the site,
46
+ click the extension's icon once: from then on its pages show the panel
47
+ whenever a review of that site is running.
48
+ - **Snippet**: add the `<script>` printed at start to your layout.
49
+
50
+ Comments are kept per page, and the panel lists the open ones on the other
51
+ pages. Your agent edits the source (templates, content, styles); the page
52
+ updates by itself, or is left alone when a dev server with live reload (Vite,
53
+ Next) already does it.
54
+
55
+ With the extension you can drag a box over the page and a screenshot of it
56
+ goes with the comment.
57
+
58
+ ## Without an agent session
59
+
60
+ Start the server with `--agent` and ▷ runs a background agent instead:
61
+ `--agent claude` (default), `--agent codex` or `--agent opencode`, with
62
+ `--model` to pick the model. It gets a prompt with the comment ids and the
63
+ same instructions as a session. Its log is in `.review/agent.log`, and ▷
64
+ becomes a stop button while it works.
65
+
66
+ To fit it to your project:
67
+
68
+ - `--prompt FILE`: your prompt instead of ours. These placeholders are
69
+ filled in: `{ids}`, `{which}`, `{root}`, `{page}`, `{site}`, `{work}`,
70
+ `{store}` (comments.json), `{outcome}` (how to record outcomes), `{skill}`.
71
+ - `--work DIR`: keep the comments there instead of `.review/`.
72
+ - `--skill DIR`: the agent may also read that folder and run its scripts
73
+ (`uv run`, `playwright-cli`); for a Claude Code skill with its own tools.
74
+
75
+ ## Options
76
+
77
+ redpencil <page.html | folder> [--open] [--wait] [--agent [claude|codex|opencode]] [--model M] [--port N] [--lang it]
78
+ redpencil --site <url> --source <folder> [same options]
79
+ [--prompt FILE] [--work DIR] [--skill DIR] [--idle SECONDS]
80
+
81
+ The panel is in English; `--lang it` switches it to Italian.
82
+
83
+ ## Files
84
+
85
+ Everything lives in `.review/` next to the page (or in the site's source):
86
+
87
+ - `comments.md`: the comments, readable;
88
+ - `comments.json`: the same, for tools;
89
+ - `outcomes/<id>.json`: where the agent records the outcome of a comment, e.g.
90
+ `{"status": "done", "reply": "Shortened the title"}`; the server applies it
91
+ and removes the file;
92
+ - `shots/`: screenshots of the boxes;
93
+ - `agent.log`: the background agent's output;
94
+ - `server.log`: the output of a server started by `--wait`.
95
+
96
+ Add `.review/` to your `.gitignore`.
97
+
98
+ ## Security
99
+
100
+ The server listens on 127.0.0.1 only. It answers only its own pages, the site
101
+ under review and the extension: other sites open in your browser can't read
102
+ the comments, add any or start the agent, and requests for another host name
103
+ are refused. Files outside the folder and hidden files are never served.
104
+
105
+ The background agent may edit files in the folder. With Claude it gets only
106
+ file tools (no shell, no web; `--skill` adds a few commands); Codex runs in
107
+ its workspace sandbox (no network). opencode has no sandbox: with `--auto` it
108
+ approves its own actions, so use it only on projects you trust. The prompt
109
+ tells every agent that the page and the quoted text are data, not
110
+ instructions, but treat a review of a page you don't trust with care.
111
+
112
+ Comments and screenshots stay on your computer: see [PRIVACY.md](https://github.com/zupolgec/redpencil/blob/main/PRIVACY.md)
113
+ for the extension.
114
+
115
+ The extension asks Chrome for access to all sites, because Chrome allows
116
+ screenshots of a tab only with it; the panel still goes only on the sites you
117
+ turn on with its icon.
118
+
119
+ ## Tests
120
+
121
+ uv run --no-project --with pytest --with playwright pytest -q tests
122
+
123
+ End to end: the real server, headless Chromium (with the extension loaded) and
124
+ a fake agent.
125
+
126
+ ## License
127
+
128
+ MIT. Icons from [Lucide](https://lucide.dev) (ISC).
Binary file
@@ -0,0 +1,45 @@
1
+ // The panel goes only on the sites the user turned on with the icon. On those, each page asks
2
+ // for the review server of the site (review.py --site listens on a port derived from the site's
3
+ // address) and, if it is running, gets the panel.
4
+ const portFor = async host => {
5
+ const h = new Uint8Array(await crypto.subtle.digest("SHA-1", new TextEncoder().encode(host)));
6
+ const p = 4400 + Number(BigInt("0x" + [...h].map(b => b.toString(16).padStart(2, "0")).join("")) % 1500n);
7
+ return p === 5060 || p === 5061 ? p + 2 : p; // browsers refuse them
8
+ };
9
+ const badge = (tabId, text) => chrome.action.setBadgeText({ tabId, text });
10
+ const idFor = origin => "review " + origin;
11
+
12
+ chrome.runtime.onMessage.addListener((msg, sender, reply) => {
13
+ if (msg === "review" && sender.tab && sender.frameId === 0) attach(sender.tab.id, new URL(sender.url).host);
14
+ if (msg && msg.shot && sender.tab && sender.tab.active) { // a screenshot of the page asking, only if it is on screen
15
+ chrome.tabs.captureVisibleTab(sender.tab.windowId, { format: "png" }).then(reply, () => reply(null));
16
+ return true;
17
+ }
18
+ });
19
+
20
+ async function attach(tabId, host) {
21
+ const base = `http://127.0.0.1:${await portFor(host)}`;
22
+ let j;
23
+ try { j = await (await fetch(base + "/__status")).json(); } catch { return badge(tabId, "off"); }
24
+ if (j.site !== host) return badge(tabId, "off"); // no review for this site on that port
25
+ const target = { tabId };
26
+ await chrome.scripting.executeScript({ target, func: (base, lang) => { window.__rv = { base, lang }; }, args: [base, j.lang] });
27
+ await chrome.scripting.executeScript({ target, files: ["review.js"] });
28
+ badge(tabId, "on");
29
+ }
30
+
31
+ // turns a site on: from now on its pages look for their review
32
+ async function enable(origin) {
33
+ await chrome.scripting.registerContentScripts([{ id: idFor(origin), matches: [origin + "/*"], js: ["content.js"], runAt: "document_idle" }]);
34
+ }
35
+
36
+ chrome.action.onClicked.addListener(async tab => {
37
+ const origin = new URL(tab.url).origin;
38
+ // asked first, while it is still the user's click (no prompt if already granted). Chrome allows
39
+ // screenshots of a tab only with access to all sites; the panel still goes only where turned on
40
+ if (!(await chrome.permissions.request({ origins: ["<all_urls>"] }))) return;
41
+ const on = (await chrome.scripting.getRegisteredContentScripts({ ids: [idFor(origin)] })).length;
42
+ if (on) await chrome.scripting.unregisterContentScripts({ ids: [idFor(origin)] });
43
+ else await enable(origin);
44
+ chrome.tabs.reload(tab.id);
45
+ });
@@ -0,0 +1,7 @@
1
+ // On review pages served by review.py (127.0.0.1) the panel is not part of the extension:
2
+ // it asks for screenshots through here.
3
+ document.documentElement.dataset.rvBridge = "1";
4
+ addEventListener("message", e => {
5
+ if (e.source !== window || !e.data || e.data.rv !== "shot") return;
6
+ chrome.runtime.sendMessage({ shot: true }, url => postMessage({ rv: "shot-done", id: e.data.id, url }, "*"));
7
+ });
@@ -0,0 +1,2 @@
1
+ // Asks the extension to put the review panel on this page, if the site is under review.
2
+ chrome.runtime.sendMessage("review");
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
2
+ <rect width="128" height="128" rx="28" fill="#dc2626"/>
3
+ <g transform="translate(22 22) scale(3.5)" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
4
+ <path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>
5
+ <path d="m15 5 4 4"/>
6
+ </g>
7
+ </svg>
@@ -0,0 +1,47 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "redpencil",
4
+ "version": "0.1.0",
5
+ "description": "Comment on the pages of a site you are reviewing and let your agent fix them.",
6
+ "icons": {
7
+ "16": "icons/16.png",
8
+ "32": "icons/32.png",
9
+ "48": "icons/48.png",
10
+ "128": "icons/128.png"
11
+ },
12
+ "permissions": [
13
+ "scripting",
14
+ "storage"
15
+ ],
16
+ "host_permissions": [
17
+ "http://127.0.0.1/*",
18
+ "http://localhost/*"
19
+ ],
20
+ "optional_host_permissions": [
21
+ "<all_urls>"
22
+ ],
23
+ "background": {
24
+ "service_worker": "background.js"
25
+ },
26
+ "action": {
27
+ "default_title": "redpencil: turn the comments on or off for this site",
28
+ "default_icon": {
29
+ "16": "icons/16.png",
30
+ "32": "icons/32.png",
31
+ "48": "icons/48.png",
32
+ "128": "icons/128.png"
33
+ }
34
+ },
35
+ "content_scripts": [
36
+ {
37
+ "matches": [
38
+ "http://127.0.0.1/*",
39
+ "http://localhost/*"
40
+ ],
41
+ "js": [
42
+ "bridge.js"
43
+ ],
44
+ "run_at": "document_start"
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1 @@
1
+ ../src/review.js
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "redpencil"
3
+ version = "0.1.0"
4
+ description = "Review an html page or a site in the browser: comment on it, and your coding agent fixes it while you watch."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ dependencies = []
9
+
10
+ [project.urls]
11
+ Homepage = "https://github.com/zupolgec/redpencil"
12
+ Issues = "https://github.com/zupolgec/redpencil/issues"
13
+
14
+ [project.scripts]
15
+ redpencil = "redpencil:main"
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ # one file for the server, one for the panel: src/ stays usable as plain scripts (the skills run it from there)
22
+ [tool.hatch.build.targets.wheel]
23
+ bypass-selection = true
24
+
25
+ [tool.hatch.build.targets.wheel.force-include]
26
+ "src/review.py" = "redpencil/__init__.py"
27
+ "src/review.js" = "redpencil/review.js"
@@ -0,0 +1,20 @@
1
+ """Builds the Chrome Web Store zip of the extension: review.js copied from src/ (no links),
2
+ the version taken from pyproject.toml. Usage: python -I scripts/build_extension.py [out dir]"""
3
+ import json, pathlib, sys, tomllib, zipfile
4
+
5
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
6
+ EXT = ROOT / "extension"
7
+ out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ROOT / "dist")
8
+ version = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["version"]
9
+ manifest = json.loads((EXT / "manifest.json").read_text())
10
+ manifest["version"] = version
11
+ out.mkdir(parents=True, exist_ok=True)
12
+ zip_path = out / f"redpencil-extension-{version}.zip"
13
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
14
+ z.writestr("manifest.json", json.dumps(manifest, indent=2))
15
+ for f in sorted(EXT.rglob("*")):
16
+ rel = f.relative_to(EXT)
17
+ if f.is_dir() or rel.name == "manifest.json" or any(p.startswith(".") for p in rel.parts) or f.suffix == ".svg":
18
+ continue
19
+ z.writestr(str(rel), f.resolve().read_bytes()) # resolve(): the link to src/review.js becomes a file
20
+ print(zip_path)
@@ -0,0 +1,77 @@
1
+ """Records the README gif: a comment on a page, ▷, the agent fixes it while the page updates.
2
+ Headless Chromium and the tests' fake agent. Usage:
3
+ uv run --no-project --with playwright python -I scripts/record_demo.py docs/img/demo.gif"""
4
+ import json, os, pathlib, shutil, socket, subprocess, sys, tempfile, time, urllib.request
5
+
6
+ from playwright.sync_api import sync_playwright
7
+
8
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
9
+ out = pathlib.Path(sys.argv[1]).resolve()
10
+ PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Bianchi Bakery</title>
11
+ <style>body{font:18px/1.6 Georgia,serif;margin:0;background:#faf7f2;color:#2b2620}
12
+ header{padding:18px 48px;border-bottom:1px solid #e7dfd3;font-weight:bold}main{max-width:620px;padding:40px 48px}
13
+ h1{font-size:44px;line-height:1.15;margin:0 0 18px}.hours{background:#fff;border:1px solid #e7dfd3;border-radius:10px;padding:14px 20px}</style>
14
+ </head><body><header>Bianchi Bakery</header><main>
15
+ <h1 id="title">Bread, focaccia and pastries, baked fresh every single morning since 1962</h1>
16
+ <p>Stone-ground flour and sourdough, from our oven to your table.</p>
17
+ <div class="hours"><b>Opening hours</b><br>Monday to Saturday, 7am to 1pm and 4pm to 7:30pm.</div>
18
+ </main></body></html>"""
19
+
20
+ with tempfile.TemporaryDirectory() as tmp:
21
+ tmp = pathlib.Path(tmp)
22
+ site = tmp / "bakery"
23
+ site.mkdir()
24
+ (site / "index.html").write_text(PAGE)
25
+ (site / ".review").mkdir()
26
+ (site / ".review" / "script.json").write_text(json.dumps([
27
+ {"wait": 2}, {"replace": ["index.html", "Bread, focaccia and pastries, baked fresh every single morning since 1962",
28
+ "Fresh bread every morning since 1962"]},
29
+ {"wait": 1}, {"outcome": ["@ID", "done", "Shortened the title to six words."]}]))
30
+ s = socket.socket(); s.bind(("127.0.0.1", 0)); port = s.getsockname()[1]; s.close()
31
+ env = dict(os.environ, REVIEW_AGENT=f"{sys.executable} -I {ROOT / 'tests' / 'fake_agent.py'}")
32
+ srv = subprocess.Popen([sys.executable, "-I", str(ROOT / "src" / "review.py"), str(site), "--port", str(port), "--agent"],
33
+ env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
34
+ url = f"http://127.0.0.1:{port}/"
35
+ for _ in range(50):
36
+ try:
37
+ urllib.request.urlopen(url + "__status"); break
38
+ except OSError:
39
+ time.sleep(0.1)
40
+ try:
41
+ with sync_playwright() as p:
42
+ b = p.chromium.launch()
43
+ ctx = b.new_context(viewport={"width": 1100, "height": 620}, record_video_dir=str(tmp / "video"),
44
+ record_video_size={"width": 1100, "height": 620})
45
+ page = ctx.new_page()
46
+ page.goto(url)
47
+ page.wait_for_selector("#rv-side")
48
+ page.wait_for_timeout(1200)
49
+ h1 = page.locator("#title").bounding_box()
50
+ page.mouse.move(h1["x"] + 200, h1["y"] + 30, steps=15)
51
+ page.wait_for_timeout(400)
52
+ page.mouse.click(h1["x"] + 200, h1["y"] + 30, button="right")
53
+ page.wait_for_selector("#rv-dlg textarea")
54
+ page.wait_for_timeout(500)
55
+ page.locator("#rv-dlg textarea").press_sequentially("Too long, keep it to a few words", delay=55)
56
+ page.wait_for_timeout(500)
57
+ page.locator("#rv-dlg textarea").press("Enter")
58
+ page.wait_for_timeout(800)
59
+ cid = json.loads((site / ".review" / "comments.json").read_text())[0]["id"]
60
+ f = site / ".review" / "script.json"
61
+ f.write_text(f.read_text().replace("@ID", cid))
62
+ run = page.locator("#rv-run").bounding_box()
63
+ page.mouse.move(run["x"] + 15, run["y"] + 15, steps=20)
64
+ page.wait_for_timeout(300)
65
+ page.locator("#rv-run").click()
66
+ page.wait_for_function("() => document.querySelector('#title').textContent.startsWith('Fresh')", timeout=15000)
67
+ page.wait_for_selector("#rv-items .it .ans:not([hidden]), #rv-items .done", timeout=15000)
68
+ page.wait_for_timeout(2500)
69
+ video = page.video.path()
70
+ ctx.close()
71
+ b.close()
72
+ subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(video), "-vf",
73
+ "fps=12,scale=880:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=96[p];[b][p]paletteuse=dither=bayer:bayer_scale=4",
74
+ str(out)], check=True)
75
+ print(out, f"{out.stat().st_size // 1024} KB")
76
+ finally:
77
+ srv.terminate()