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.
- redpencil-0.1.0/.github/workflows/tests.yml +19 -0
- redpencil-0.1.0/.gitignore +12 -0
- redpencil-0.1.0/CHANGELOG.md +18 -0
- redpencil-0.1.0/LICENSE +23 -0
- redpencil-0.1.0/PKG-INFO +139 -0
- redpencil-0.1.0/PRIVACY.md +23 -0
- redpencil-0.1.0/README.md +128 -0
- redpencil-0.1.0/docs/img/demo.gif +0 -0
- redpencil-0.1.0/extension/background.js +45 -0
- redpencil-0.1.0/extension/bridge.js +7 -0
- redpencil-0.1.0/extension/content.js +2 -0
- redpencil-0.1.0/extension/icons/128.png +0 -0
- redpencil-0.1.0/extension/icons/16.png +0 -0
- redpencil-0.1.0/extension/icons/32.png +0 -0
- redpencil-0.1.0/extension/icons/48.png +0 -0
- redpencil-0.1.0/extension/icons/icon.svg +7 -0
- redpencil-0.1.0/extension/manifest.json +47 -0
- redpencil-0.1.0/extension/review.js +1 -0
- redpencil-0.1.0/pyproject.toml +27 -0
- redpencil-0.1.0/scripts/build_extension.py +20 -0
- redpencil-0.1.0/scripts/record_demo.py +77 -0
- redpencil-0.1.0/src/review.js +595 -0
- redpencil-0.1.0/src/review.py +631 -0
- redpencil-0.1.0/tests/conftest.py +162 -0
- redpencil-0.1.0/tests/fake_agent.py +29 -0
- redpencil-0.1.0/tests/page/img/a.svg +1 -0
- redpencil-0.1.0/tests/page/index.html +14 -0
- redpencil-0.1.0/tests/page/other.html +3 -0
- redpencil-0.1.0/tests/test_agents.py +53 -0
- redpencil-0.1.0/tests/test_area.py +75 -0
- redpencil-0.1.0/tests/test_dynamic.py +80 -0
- redpencil-0.1.0/tests/test_extension_package.py +39 -0
- redpencil-0.1.0/tests/test_fixes.py +133 -0
- redpencil-0.1.0/tests/test_lifecycle.py +81 -0
- redpencil-0.1.0/tests/test_package.py +24 -0
- redpencil-0.1.0/tests/test_panel.py +248 -0
- redpencil-0.1.0/tests/test_panel_ux.py +55 -0
- redpencil-0.1.0/tests/test_site.py +130 -0
- 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,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`.
|
redpencil-0.1.0/LICENSE
ADDED
|
@@ -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.
|
redpencil-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

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

|
|
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
|
+
});
|
|
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()
|