@vintasoftware/pr-review-canvas 0.2.0 → 0.4.0

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 (138) hide show
  1. package/README.md +61 -26
  2. package/docs/reference.md +231 -121
  3. package/package.json +18 -4
  4. package/pr-review.config.example.yml +10 -4
  5. package/prompts/chat-seed.md +3 -0
  6. package/prompts/generation-format.md +3 -0
  7. package/skills/pr-review-canvas/SKILL.md +77 -40
  8. package/src/acpx/acpx.ts +21 -4
  9. package/src/acpx/events.ts +5 -1
  10. package/src/acpx/ndjson.ts +3 -1
  11. package/src/acpx/preflight.ts +5 -1
  12. package/src/canvas/comment.ts +24 -0
  13. package/src/canvas/export.ts +11 -2
  14. package/src/canvas/import.ts +30 -9
  15. package/src/canvas/name.ts +1 -0
  16. package/src/canvas/zip.ts +25 -2
  17. package/src/chat/chat-manager.ts +55 -41
  18. package/src/chat/context.ts +10 -3
  19. package/src/chat/seed.ts +3 -1
  20. package/src/chat/threads.ts +27 -26
  21. package/src/cli.ts +24 -9
  22. package/src/commands.ts +86 -25
  23. package/src/config.ts +24 -24
  24. package/src/contract/api.ts +32 -1
  25. package/src/contract/canvas-manifest.ts +2 -0
  26. package/src/contract/comments.ts +8 -1
  27. package/src/contract/discovery.ts +5 -2
  28. package/src/contract/generation-context.ts +27 -2
  29. package/src/contract/review-artifact.ts +14 -8
  30. package/src/contract/review-key.ts +51 -0
  31. package/src/contract/reviews.ts +17 -0
  32. package/src/contract/settings.ts +2 -0
  33. package/src/contract/state.ts +41 -19
  34. package/src/git/diff-collector.ts +2 -1
  35. package/src/git/environment.mjs +27 -0
  36. package/src/git/git.ts +117 -12
  37. package/src/git/local-target.ts +138 -0
  38. package/src/git/patch-lines.ts +34 -2
  39. package/src/git/pr-refs.ts +36 -0
  40. package/src/github/attachments.ts +9 -257
  41. package/src/github/canvas-comment.ts +22 -0
  42. package/src/github/capabilities.ts +3 -37
  43. package/src/github/comments.ts +9 -27
  44. package/src/github/post-comment.ts +7 -37
  45. package/src/github/post-review.ts +4 -19
  46. package/src/github/pr.ts +6 -84
  47. package/src/github/threads.ts +6 -2
  48. package/src/gitlab/attachments.ts +40 -0
  49. package/src/gitlab/canvas-comment.ts +26 -0
  50. package/src/gitlab/capabilities.ts +64 -0
  51. package/src/gitlab/comments.ts +164 -0
  52. package/src/gitlab/mr.ts +115 -0
  53. package/src/gitlab/post-comment.ts +111 -0
  54. package/src/gitlab/post-review.ts +54 -0
  55. package/src/gitlab/project.ts +13 -0
  56. package/src/host/attachments.ts +293 -0
  57. package/src/host/capabilities.ts +38 -0
  58. package/src/host/client.ts +245 -0
  59. package/src/host/host.ts +136 -0
  60. package/src/host/pr.ts +51 -0
  61. package/src/host/remote.ts +42 -0
  62. package/src/project-config.ts +25 -9
  63. package/src/prompt-files.ts +4 -3
  64. package/src/review/carry-over.ts +79 -0
  65. package/src/review/doctor.ts +31 -14
  66. package/src/review/normalize.ts +3 -1
  67. package/src/review/prepare.ts +87 -17
  68. package/src/review/prompt.ts +8 -2
  69. package/src/review/publish.ts +76 -13
  70. package/src/{github → review}/review-body.ts +17 -5
  71. package/src/review/skill-command.ts +5 -3
  72. package/src/review/trim-caps.ts +10 -6
  73. package/src/review/validate-folds.ts +2 -2
  74. package/src/review/validate.ts +67 -15
  75. package/src/server/app.ts +12 -4
  76. package/src/server/bundle.ts +320 -106
  77. package/src/server/context.ts +24 -10
  78. package/src/server/errors.ts +56 -10
  79. package/src/server/html.ts +36 -12
  80. package/src/server/node-server.ts +4 -2
  81. package/src/server/routes/api.ts +92 -31
  82. package/src/server/routes/chat-routes.ts +105 -46
  83. package/src/server/routes/pages.ts +25 -9
  84. package/src/server/routes/review-routes.ts +101 -35
  85. package/src/server/sse.ts +3 -1
  86. package/src/store/atomic-json.ts +5 -1
  87. package/src/store/canvas-store.ts +95 -46
  88. package/src/store/data-dir.ts +2 -1
  89. package/src/store/derived-store.ts +41 -23
  90. package/src/store/pr-store.ts +25 -15
  91. package/src/store/state-store.ts +42 -37
  92. package/static/brand.svg +19 -0
  93. package/static/js/api.js +47 -27
  94. package/static/js/app.js +27 -7
  95. package/static/js/chat-panel.js +93 -0
  96. package/static/js/chat.js +41 -17
  97. package/static/js/composer.js +30 -17
  98. package/static/js/contract-types.d.ts +3 -0
  99. package/static/js/diagram.js +2 -1
  100. package/static/js/diff-decorations.js +5 -3
  101. package/static/js/diff-renderer.js +7 -2
  102. package/static/js/dom.js +5 -7
  103. package/static/js/download.js +1 -1
  104. package/static/js/empty-state.js +85 -16
  105. package/static/js/errors.js +22 -6
  106. package/static/js/header.js +32 -9
  107. package/static/js/host.js +40 -0
  108. package/static/js/import-zone.js +1 -1
  109. package/static/js/interactions.js +56 -17
  110. package/static/js/keyboard.js +7 -2
  111. package/static/js/layers.js +26 -14
  112. package/static/js/links.js +9 -3
  113. package/static/js/markdown.js +28 -1
  114. package/static/js/nav.js +5 -2
  115. package/static/js/overview.js +32 -4
  116. package/static/js/points.js +5 -3
  117. package/static/js/progress.js +2 -1
  118. package/static/js/proposed-comment.js +4 -1
  119. package/static/js/quick-questions.js +2 -1
  120. package/static/js/regenerate.js +4 -1
  121. package/static/js/review-session.js +7 -2
  122. package/static/js/settings.js +2 -1
  123. package/static/js/signoff.js +9 -6
  124. package/static/styles/base.css +16 -6
  125. package/static/styles/chat-panel.css +81 -0
  126. package/static/styles/chat-tools.css +28 -0
  127. package/static/styles/chat.css +1 -1
  128. package/static/styles/commands.css +10 -4
  129. package/static/styles/diff.css +1 -1
  130. package/static/styles/header.css +18 -4
  131. package/static/styles/layout.css +4 -4
  132. package/static/styles/panels.css +4 -0
  133. package/static/styles/responsive.css +1 -15
  134. package/static/styles/review-actions.css +1 -0
  135. package/static/styles/review.css +24 -3
  136. package/static/styles/skin-github.css +99 -100
  137. package/static/styles.css +13 -12
  138. package/src/github/gh.ts +0 -196
package/docs/reference.md CHANGED
@@ -15,17 +15,20 @@ For setup and the basic review workflow, see the [README](../README.md).
15
15
 
16
16
  ### Repository and runtime options
17
17
 
18
- | Option | Applies to | Default and behavior |
19
- |---|---|---|
20
- | `--repo <dir>` | All commands | Uses the current directory when omitted; resolves the repository root from there |
21
- | `--data-dir <dir>` | All except `install-skill` | Overrides `PR_REVIEW_DATA_DIR`, then the default `<main checkout>/.pr-review` |
22
- | `--port <n>` | `serve` | Overrides `PR_REVIEW_PORT`, then `3010`; accepts 1–65535 |
23
- | `--agent claude\|codex` | `serve` | Overrides the saved chat agent for this run |
24
- | `--model <id>` | `serve` | Overrides the saved chat model for this run |
25
- | `--fixture-canvas <review.json>` | `serve` | Development preview: uses the supplied canvas for every requested PR, with its head replaced by the live PR head |
26
-
27
- Repository operations require an `origin` remote on **github.com**. GitHub Enterprise Server
28
- hosts are not supported. Fetching a PR does not check out its branch.
18
+ | Option | Applies to | Default and behavior |
19
+ | -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
20
+ | `--repo <dir>` | All commands | Uses the current directory when omitted; resolves the repository root from there |
21
+ | `--data-dir <dir>` | All except `install-skill` | Overrides `PR_REVIEW_DATA_DIR`, then the default `<main checkout>/.pr-review` |
22
+ | `--port <n>` | `serve` | Overrides `PR_REVIEW_PORT`, then `3010`; accepts 1–65535 |
23
+ | `--agent claude\|codex` | `serve` | Overrides the saved chat agent for this run |
24
+ | `--model <id>` | `serve` | Overrides the saved chat model for this run |
25
+ | `--fixture-canvas <review.json>` | `serve` | Development preview: uses the supplied canvas for every requested PR, with its head replaced by the live PR head |
26
+ | `PR_REVIEW_HOST=gitlab` | Environment | Treats a non-github.com origin as GitLab (self-hosted hosts whose name does not contain `gitlab`) |
27
+
28
+ Repository operations require an `origin` remote on **github.com** or **GitLab** (gitlab.com, a
29
+ hostname that contains `gitlab`, or any host with `PR_REVIEW_HOST=gitlab`). GitHub Enterprise Server
30
+ hosts are not supported. Fetching a PR or merge request does not check out its branch. Use `--pr`
31
+ for both GitHub pull request numbers and GitLab merge request IIDs.
29
32
 
30
33
  ### Prepare, validate, and publish
31
34
 
@@ -33,7 +36,7 @@ These commands support custom generation workflows. The bundled
33
36
  [generation skill](../skills/pr-review-canvas/SKILL.md) describes the complete sequence and model rules.
34
37
 
35
38
  ```text
36
- pr-review prepare (--pr <n> | --base <ref> --head <ref>) [--force]
39
+ pr-review prepare (--pr <n> | --branch | --uncommitted | --base <ref> --head <ref>) [--base <ref>] [--force]
37
40
  pr-review validate <model.json|review.json> --canvas <dir> [--human] [--fix]
38
41
  pr-review publish <canvasDir> --agent <id> [--model <id>] --harness claude-code|codex|other [--allow-stale]
39
42
  ```
@@ -43,7 +46,45 @@ A status of `exists` means that head already has a canvas. With `--force`, prepa
43
46
  previous generation's working files while keeping the published canvas available until a new
44
47
  publish succeeds.
45
48
 
46
- For a comparison before a PR exists, use local refs:
49
+ ### Reviewing before the pull request exists
50
+
51
+ There are two reviews of the work in a clone, and they are separate targets:
52
+
53
+ ```bash
54
+ pr-review prepare --branch # served at /review/branch
55
+ pr-review prepare --uncommitted # served at /review/uncommitted
56
+ ```
57
+
58
+ `--branch` describes the tip of the current branch. `--uncommitted` describes the working tree as
59
+ it stands, with the edits and the untracked files on top of that tip; with a clean tree the two
60
+ build the same canvas. Each keeps its own canvas, review progress and chat threads, so preparing
61
+ one never disturbs the other.
62
+
63
+ - **Base.** Both compare against the repository's default branch, resolved from `origin/HEAD` and
64
+ falling back to `origin/main`, `origin/master`, `main`, then `master`. `--base <ref>` overrides
65
+ it. Preparation fails with a hint when none of them resolve.
66
+ - **Uncommitted work.** `--uncommitted` stages the working tree into an index of its own and
67
+ writes a commit from it, so the diff covers files that are not committed yet. Nothing the user
68
+ staged is touched, ignored files stay out, and the commit is anchored at
69
+ `refs/worktree/pr-review-snapshot` so `git gc` cannot collect it before `publish`. Publishing
70
+ gives that canvas an anchor of its own under `refs/worktree/pr-review-canvas/`, so a later
71
+ snapshot cannot leave it collectable. The same working tree always hashes to the same commit.
72
+ - **Staleness.** Committing after `--branch`, or editing a file after `--uncommitted`, moves the
73
+ head, so `publish` answers `CANVAS_STALE`, exactly as a push does for a pull request. The page
74
+ reads the head again when it is opened and when `refresh` is pressed, and offers to regenerate.
75
+ Its background polls answer about that same head, so they never contradict what the page shows;
76
+ they read the work again only once a new canvas has been prepared.
77
+ - **Worktrees.** The snapshot index and its anchor are per worktree, so two worktrees of one clone
78
+ never overwrite each other's snapshot. The review targets are not: `branch` and `uncommitted`
79
+ name one review per clone, so worktrees share their canvas, review progress and chat threads.
80
+ Review local work from one worktree at a time.
81
+ - **No forge side.** A local canvas posts nothing: comments, sign-off, canvas import, and
82
+ attachment discovery are refused for it. The page draws no import drop zone and no shared-canvas
83
+ callout, and the comment and sign-off commands stay disabled with the reason. A canvas of a working-tree
84
+ snapshot is never offered as a pull request's canvas, or as the branch review's, because its
85
+ commit is on no branch.
86
+
87
+ For a comparison between two commits that both exist, name them instead:
47
88
 
48
89
  ```bash
49
90
  pr-review prepare --base origin/main --head HEAD
@@ -54,7 +95,8 @@ pr-review prepare --base origin/main --head HEAD
54
95
  the explanation after the first `:` or `—` and reports the changes. Titles that still exceed the
55
96
  limit and overlong prose require rewriting.
56
97
 
57
- `publish` returns `status`, `headSha`, `reviewJsonPath`, `attempts`, and a `reviewUrl` for PR runs.
98
+ `publish` returns `status`, `headSha`, `reviewJsonPath`, `attempts`, `sharing`, and a `reviewUrl`
99
+ for PR and local runs.
58
100
  Its `--agent`, `--model`, and `--harness` describe who generated the canvas; they do not launch or
59
101
  select an agent. `--allow-stale` permits publishing for the prepared commit after the PR head has
60
102
  moved. Use it only when that older commit is the intended review target.
@@ -63,6 +105,35 @@ For more than 400 changed files or 50,000 added/deleted lines, preparation leave
63
105
  prompt and directs the generator to read patch files individually. Smaller diffs are inlined up
64
106
  to `generation.inlineDiffMaxLines`.
65
107
 
108
+ ### Automatic sharing and ZIP fallback
109
+
110
+ For PR/MR targets, `publish` saves the validated canvas locally, then posts its compressed ZIP
111
+ as base64 inside a hidden HTML comment on GitHub or GitLab. The visible comment identifies the
112
+ commit and explains how to open the canvas. Publishing again updates the existing canvas comment
113
+ owned by the current CLI account; another author's comment is left alone. The payload contains
114
+ the same `manifest.json` and `review.json` as an export, including the PR/MR description and review
115
+ notes. Hidden markup is not private: anyone who can read the comment can retrieve the payload.
116
+ No generated files enter Git history and no storage service or CI workflow is required.
117
+
118
+ Check the `sharing` result even when the process exits successfully:
119
+
120
+ - `{ "status": "shared", "url": "..." }`: the host accepted the comment.
121
+ - `{ "status": "failed", "warning": "...", "zipPath": "..." }`: the canvas is saved locally,
122
+ but automatic sharing failed. The CLI also prints a warning to stderr and exports a fallback ZIP.
123
+ Upload that ZIP into the PR/MR description in the browser and save; replace an older attachment
124
+ link if present. A host failure can have an uncertain outcome, so check the comment before retrying.
125
+ - `{ "status": "local" }`: a refs-only target has no PR/MR to publish to.
126
+
127
+ The entire comment must fit the host limit: 65,536 characters on GitHub and 1,000,000 on GitLab.
128
+ Base64 uses roughly four characters per three compressed bytes, leaving slightly under 48 KiB
129
+ for a GitHub ZIP after the envelope and visible text. Oversize canvases are never split or truncated.
130
+ Permission, login, network, and host-policy errors use the same ZIP fallback. A sharing failure
131
+ keeps exit code 0 because local publication succeeded; validation errors still exit 5.
132
+ The generation skill must report the warning and manual-upload instructions on sharing failure.
133
+
134
+ `export` remains a local-only command for backups and manual sharing. For refs-only canvases,
135
+ export with `--head <sha> --pr <n>` once the PR exists, or regenerate for the PR to share automatically.
136
+
66
137
  ### Export and import options
67
138
 
68
139
  ```text
@@ -79,16 +150,21 @@ pr-review import <zip> [--pr <n>] [--force]
79
150
  canvas generation time in UTC, to seconds, so exports sort chronologically within each PR.
80
151
  Before a PR exists, `ref-` replaces `pr-<number>-`. Re-exporting the same canvas keeps its name.
81
152
  - Export returns `status`, `path`, `name`, `headSha`, and `prNumber` when supplied or stored.
82
- - `import --pr` compares the imported canvas with that PR's current head. Without it, import does
83
- not check against a live PR.
153
+ - `import --pr` compares the imported canvas with that PR's current head, and refuses a canvas
154
+ exported for a different pull request with `CANVAS_PR_MISMATCH`. `--force` does not lift that
155
+ refusal: a canvas is stored under the pull request it names, so importing the same ZIP without
156
+ `--pr` stores it under its own PR instead. Without `--pr`, import does not check against a live
157
+ PR. A canvas generated before the PR existed names none and joins the pull request it is
158
+ imported for.
84
159
  - Import returns `ready`, `stale`, or `exists`, plus commit information and warnings. `exists`
85
160
  keeps a stored canvas generated at the same time or later. `derivable: false` means the canvas
86
161
  was accepted but its source diffs could not be rebuilt from Git.
87
162
  - `import --force` allows a canvas from another repository. It does not force an older canvas to
88
- replace a newer one.
163
+ replace a newer one, and does not allow a canvas of another pull request.
89
164
 
90
165
  Imports accept archives up to **20 MiB**. The required `manifest.json` and `review.json` entries
91
- must be at the archive root and pass format validation. If the necessary commits are missing,
166
+ must be at the archive root, pass format validation, and agree on the commit and the pull request
167
+ they describe. If the necessary commits are missing,
92
168
  the tool attempts to fetch them; a failed fetch can leave the notes available without diffs.
93
169
 
94
170
  ### Skill installation options
@@ -129,13 +205,13 @@ JSON error. `serve` stays running and writes its startup message to stderr.
129
205
  Command failures use `{ "error": { "code", "message", "hint" } }`, with `hint` optional.
130
206
  Validation failures from `validate` use its report format instead.
131
207
 
132
- | Exit code | Meaning |
133
- |---|---|
134
- | `0` | Success |
135
- | `1` | Error, including a failed `doctor` check |
136
- | `2` | Command usage error, such as an unknown command or missing required flag |
137
- | `4` | GitHub CLI missing or unauthenticated |
138
- | `5` | Validation failed in `validate` or `publish` |
208
+ | Exit code | Meaning |
209
+ | --------- | ------------------------------------------------------------------------ |
210
+ | `0` | Success |
211
+ | `1` | Error, including a failed `doctor` check |
212
+ | `2` | Command usage error, such as an unknown command or missing required flag |
213
+ | `4` | GitHub CLI (`gh`) or GitLab CLI (`glab`) missing or unauthenticated |
214
+ | `5` | Validation failed in `validate` or `publish` |
139
215
 
140
216
  `doctor` reports failed checks with exit `1`, including authentication failures.
141
217
 
@@ -148,20 +224,21 @@ Lists you supply replace their defaults.
148
224
  Path patterns match repository-relative paths. `**` crosses directories; `*` and `?` match
149
225
  within one path segment.
150
226
 
151
- | Key | Default | Details |
152
- |---|---|---|
153
- | `version` | `1` | The only supported configuration version |
154
- | `rulebook` | Unset | Path to a Markdown file of project code standards, resolved from the repository root; these standards take precedence over bundled standards |
155
- | `layers` | `[]` | Optional review guidance; each entry has `id`, `title`, `description`, and optional `paths` patterns. The agent may combine, split, or reorder groups. When omitted or empty, it chooses semantic sections from the change |
156
- | `highRisk` | `[]` | Entries with a `pattern` glob and `label`; matching changes receive risk labels and cannot go in the Other layer |
157
- | `generation.mode` | `strict` | See [generation modes](#generation-modes) |
158
- | `generation.maxRepairRounds` | `3` | Failed validation rounds allowed by the generation skill |
159
- | `generation.inlineDiffMaxLines` | `1500` | Maximum diff length to include directly in the generation prompt |
160
- | `generation.smallPrHunks` | `10` | At or below this hunk count, the prompt asks for one layer unless concerns differ |
161
- | `generation.caps` | See below | Overrides individual text limits |
162
- | `tests.patterns` | `['**/*.test.*', '**/*.spec.*', '**/__tests__/**']` | Paths treated as tests for review ordering and labels |
163
- | `chat.enabled` | `true` | Set to `false` to disable AI Chat |
164
- | `prompts` | Bundled templates | See [prompt templates](#prompt-templates) for supported keys and behavior |
227
+ | Key | Default | Details |
228
+ | ------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
229
+ | `version` | `1` | The only supported configuration version |
230
+ | `rulebook` | Unset | Path to a Markdown file of project code standards, resolved from the repository root; these standards take precedence over bundled standards |
231
+ | `layers` | `[]` | Optional review guidance; each entry has `id`, `title`, `description`, and optional `paths` patterns. The agent may combine, split, or reorder groups. When omitted or empty, it chooses semantic sections from the change |
232
+ | `highRisk` | `[]` | Entries with a `pattern` glob and `label`; matching changes receive risk labels and cannot go in the Other layer |
233
+ | `generation.mode` | `strict` | See [generation modes](#generation-modes) |
234
+ | `generation.maxRepairRounds` | `3` | Failed validation rounds allowed by the generation skill |
235
+ | `generation.inlineDiffMaxLines` | `1500` | Maximum diff length to include directly in the generation prompt |
236
+ | `generation.smallPrHunks` | `10` | At or below this hunk count, the prompt asks for one layer unless concerns differ |
237
+ | `generation.caps` | See below | Overrides individual text limits |
238
+ | `tests.patterns` | `['**/*.test.*', '**/*.spec.*', '**/__tests__/**']` | Paths treated as tests for review ordering and labels |
239
+ | `chat.enabled` | `true` | Set to `false` to disable AI Chat |
240
+ | `canvas.keepForIdenticalDiff` | `true` | Keep the canvas current for a later head whose diff is identical to the canvas's; see [outdated canvases](#outdated-canvases). Set to `false` to mark it outdated on every commit |
241
+ | `prompts` | Bundled templates | See [prompt templates](#prompt-templates) for supported keys and behavior |
165
242
 
166
243
  Generation's numeric options and text caps must be positive integers. An empty `layers` list
167
244
  provides no suggested groups; an empty `tests.patterns` list recognizes no files as tests.
@@ -182,14 +259,14 @@ affects canvas generation; chat answers the reviewer's selected question.
182
259
 
183
260
  The `prompts` map in `pr-review.config.yml` accepts these keys:
184
261
 
185
- | Key | Purpose |
186
- |---|---|
187
- | `generation-format.md` | Schema and output rules |
188
- | `generation-strict.md` | Instructions for strict mode |
189
- | `generation-surfacing.md` | Instructions for surfacing mode |
190
- | `quality-standards.md` | Bundled code standards |
191
- | `layering-guidance.md` | Guidance for grouping related changes |
192
- | `chat-seed.md` | Opening AI Chat instructions |
262
+ | Key | Purpose |
263
+ | ------------------------- | ------------------------------------- |
264
+ | `generation-format.md` | Schema and output rules |
265
+ | `generation-strict.md` | Instructions for strict mode |
266
+ | `generation-surfacing.md` | Instructions for surfacing mode |
267
+ | `quality-standards.md` | Bundled code standards |
268
+ | `layering-guidance.md` | Guidance for grouping related changes |
269
+ | `chat-seed.md` | Opening AI Chat instructions |
193
270
 
194
271
  Each configured file replaces a whole template. Paths resolve from the project root,
195
272
  including when running from a subdirectory or using `--repo`. Absolute paths work for
@@ -214,18 +291,18 @@ when upgrading.
214
291
  Set any of these keys under `generation.caps`. Prose limits count visible characters, so Markdown
215
292
  link targets do not count. `diagram` counts raw Mermaid source characters.
216
293
 
217
- | Key | Default |
218
- |---|---|
219
- | `summary` | 1200 |
220
- | `layerTitle` | 60 |
221
- | `rationale` | 300 |
222
- | `decisions` | 600 |
223
- | `checkByHand` | 400 |
224
- | `annotation` | 240 |
225
- | `pointTitle` | 90 |
226
- | `pointBody` | 600 |
227
- | `testBehavior` | 120 |
228
- | `diagram` | 1500 |
294
+ | Key | Default |
295
+ | -------------- | ------- |
296
+ | `summary` | 1200 |
297
+ | `layerTitle` | 60 |
298
+ | `rationale` | 300 |
299
+ | `decisions` | 600 |
300
+ | `checkByHand` | 400 |
301
+ | `annotation` | 240 |
302
+ | `pointTitle` | 90 |
303
+ | `pointBody` | 600 |
304
+ | `testBehavior` | 120 |
305
+ | `diagram` | 1500 |
229
306
 
230
307
  The canvas has a separate limit of 12 attention points, including entries generated from missing
231
308
  tests. Increasing text caps does not increase that limit.
@@ -236,7 +313,7 @@ Custom patterns replace the JavaScript/TypeScript defaults. For example, a Pytho
236
313
 
237
314
  ```yaml
238
315
  tests:
239
- patterns: ['**/test_*.py', '**/tests/**']
316
+ patterns: ['**/test_*.py', '**/tests/**']
240
317
  ```
241
318
 
242
319
  Test recognition controls ordering, but matching a test to its source file currently follows
@@ -247,15 +324,15 @@ placed in Other while its source is in a regular layer.
247
324
 
248
325
  The data directory's `settings.yml` accepts these keys and values:
249
326
 
250
- | Key | Default | Accepted values |
251
- |---|---|---|
252
- | `version` | `1` | `1` |
253
- | `skin` | `terminal` | `terminal`, `github` |
254
- | `theme` | `auto` | `auto`, `light`, `dark` |
255
- | `agent` | `claude` | `claude`, `codex` |
256
- | `model` | `null` | A model ID, or `null` for the agent's default |
257
- | `chatTimeoutSec` | `600` | Integer seconds, 30–3600 |
258
- | `maxTurns` | `null` | Integer 1–100, or `null` for the agent's default |
327
+ | Key | Default | Accepted values |
328
+ | ---------------- | ---------- | ------------------------------------------------ |
329
+ | `version` | `1` | `1` |
330
+ | `skin` | `terminal` | `terminal`, `github` |
331
+ | `theme` | `auto` | `auto`, `light`, `dark` |
332
+ | `agent` | `claude` | `claude`, `codex` |
333
+ | `model` | `null` | A model ID, or `null` for the agent's default |
334
+ | `chatTimeoutSec` | `600` | Integer seconds, 30–3600 |
335
+ | `maxTurns` | `null` | Integer 1–100, or `null` for the agent's default |
259
336
 
260
337
  Invalid settings fall back to defaults. URL parameters `?skin=github&theme=light` can override
261
338
  appearance for one page load without saving it.
@@ -264,8 +341,8 @@ By default, Git worktrees of the same clone share the main checkout's data direc
264
341
  clones have separate data. An explicit data-directory override also relocates `settings.yml`,
265
342
  canvases, review progress, and chat history.
266
343
 
267
- The data directory contains exported archives, saved canvases, generation inputs, cached GitHub
268
- data, source diffs, and personal review progress. Its own `.gitignore` excludes its contents.
344
+ The data directory contains exported archives, saved canvases, generation inputs, cached GitHub or
345
+ GitLab data, source diffs, and personal review progress. Its own `.gitignore` excludes its contents.
269
346
  Deleting the directory loses saved preferences, canvases, progress, and chat history.
270
347
 
271
348
  ## Review controls
@@ -274,13 +351,13 @@ Deleting the directory loses saved preferences, canvases, progress, and chat his
274
351
 
275
352
  Append a fragment to `/review/<pr-number>`:
276
353
 
277
- | Target | Fragment example |
278
- |---|---|
279
- | Layer | `#layer:data-access` |
280
- | File | `#file:src/store.ts` |
281
- | Hunk | `#hunk:src/store.ts#2` |
282
- | New-side line range | `#line:src/store.ts:40-52` |
283
- | Old-side line | `#line:src/store.ts:40:old` |
354
+ | Target | Fragment example |
355
+ | ------------------- | --------------------------- |
356
+ | Layer | `#layer:data-access` |
357
+ | File | `#file:src/store.ts` |
358
+ | Hunk | `#hunk:src/store.ts#2` |
359
+ | New-side line range | `#line:src/store.ts:40-52` |
360
+ | Old-side line | `#line:src/store.ts:40:old` |
284
361
 
285
362
  Use the layer key and file path from the canvas. Links open the target file when needed; if a
286
363
  line is unavailable, navigation uses the nearest visible row. Recipients need their own running
@@ -298,32 +375,61 @@ it automatically.
298
375
 
299
376
  ### Finding shared canvases
300
377
 
301
- Discovery checks the PR description and comments for canvas ZIP links. It prefers a filename
378
+ Discovery reads compressed canvas comments and checks the PR description and comments for legacy
379
+ canvas ZIP links. Both use the same ZIP validation and import path. It prefers a filename
302
380
  matching the current head, then the PR number, then the most recently edited source text.
303
- If a download fails, it tries other matching attachments. Keep the exported filename so the
304
- canvas can be recognized.
381
+ If a candidate fails, it tries other matching canvases. Keep the exported filename so the
382
+ canvas can be recognized. **Refresh** also checks for regenerated canvases at the same head commit.
383
+
384
+ An attachment exported for a different pull request is reported rather than imported, and is not
385
+ downloaded at all when its filename already names the other PR. The page says so and offers the
386
+ drop zone, which applies the same check: a ZIP whose name or manifest belongs to another PR is
387
+ refused.
305
388
 
306
- When automatic download fails, download the archive in GitHub's UI and use the page's drop zone
307
- or `pr-review import <zip> --pr <n>`.
389
+ When automatic download fails, download the archive in GitHub or GitLab and use the page's drop
390
+ zone or `pr-review import <zip> --pr <n>`.
308
391
 
309
392
  ### Comments and sign-off
310
393
 
311
394
  You can post inline comments, replies, PR-level comments, and attention points. Inline comments
312
- must target lines in the diff. Posting uses your GitHub CLI account and remains subject to its
313
- repository permissions.
395
+ must target lines in the diff. Posting uses your `gh` or `glab` account and remains subject to its
396
+ repository permissions. On GitLab, **request changes** posts the review body as a merge request
397
+ note; **approve** calls GitLab's approve API.
314
398
 
315
399
  The sign-off dialog previews an editable review body summarizing reviewed layers, dismissed
316
400
  attention points, and comments posted from the canvas. Approval requires every layer except
317
401
  **Other changes** to be reviewed for the current head. Requesting changes does not require that
318
402
  completion. If the head moves before submission, reload and review the current commit.
319
403
 
404
+ ### Outdated canvases
405
+
406
+ A canvas describes one head commit. When the pull request moves to another commit, the page
407
+ shows **Canvas is outdated**, offers the older canvas read-only, and disables posting from it.
408
+
409
+ An identical diff is the exception. When the head's diff against its merge base is the same as
410
+ the canvas commit's diff against its own, file by file and byte for byte, the canvas is carried
411
+ over: the page shows it under a **Canvas still applies** note, review progress carries over, and
412
+ sign-off, comments, and AI Chat keep working. The note says how many commits later the head is,
413
+ when the head was built on the canvas's commit. Merging the base branch in (**Update branch**) keeps
414
+ the diff identical as long as the base did not touch the changed files. The rule does not care how
415
+ the head reached that diff, only that it did; a diff that differs anywhere, even a hunk moved down
416
+ by a base change, marks the canvas outdated as before, because the canvas's layers, hunk ids, and
417
+ attention points describe the diff it was generated from. It needs `canvas.keepForIdenticalDiff`
418
+ (the default) and both commits in the local clone: the canvas's own commit has to be there for its
419
+ diff to be rebuilt, and without that diff there is nothing to compare, so the canvas reads outdated.
420
+ Two empty change sets are not compared either. `pr-review publish` and `pr-review import` apply the
421
+ same rule, so the CLI never calls a canvas stale that the page shows as current.
422
+
423
+ AI Chat also answers on an outdated canvas: it quotes the diff of the canvas's own commit, the
424
+ one on screen.
425
+
320
426
  ## AI Chat
321
427
 
322
428
  **Ask** on a layer, file, attention point, or line selection chooses the context for your message.
323
429
  Choosing another target replaces it; **clear** returns to the whole PR. The `a` key asks about
324
430
  the focused target, and `/` focuses the message box.
325
431
 
326
- Chat can propose an inline GitHub comment. A valid proposal appears with controls to post, edit,
432
+ Chat can propose an inline comment. A valid proposal appears with controls to post, edit,
327
433
  or copy it. A proposal outside the current diff remains text with an explanation.
328
434
 
329
435
  Use **stop** to interrupt a reply. Only one chat turn can run per PR at a time. A timeout or
@@ -332,9 +438,10 @@ incomplete answer can be retried; increase `chatTimeoutSec` if replies need more
332
438
  ## Network access and permissions
333
439
 
334
440
  The server binds to `127.0.0.1` and rejects browser writes from other origins. It is intended for
335
- local use with your GitHub login.
441
+ local use with your GitHub or GitLab login.
336
442
 
337
- GitHub requests fetch PR data and attachments and submit the comments or reviews you choose to
443
+ Publishing sends the canvas as a PR/MR comment. Host requests also fetch PR or MR data and
444
+ attachments and submit the comments or reviews you choose to
338
445
  post. Rendered Markdown can load images from HTTPS hosts. Generation and chat send review
339
446
  context to the selected coding agent and its configured provider.
340
447
 
@@ -344,41 +451,44 @@ sandbox for the agent. Its access also depends on the agent's own permissions. D
344
451
 
345
452
  ## Troubleshooting
346
453
 
347
- | Symptom or code | Next step |
348
- |---|---|
349
- | `NOT_A_REPO` | Run inside a Git clone or pass `--repo <dir>` |
350
- | `NO_ORIGIN` | Check that `origin` points to a repository on github.com |
351
- | `GH_MISSING` / `GH_UNAUTHENTICATED` | Install [GitHub CLI](https://cli.github.com), run `gh auth login`, and check authentication in the same environment that runs the server |
352
- | `GITHUB_API_ERROR` | Read the underlying error for permissions, rate limits, connectivity, or GitHub service problems |
353
- | `PR_NOT_FOUND` | Check the PR number, repository, and your access |
354
- | `CANVAS_NOT_FOUND` | Generate or import a canvas for the requested commit |
355
- | `CANVAS_INVALID` | Read the format errors; re-export or regenerate the canvas |
356
- | `CANVAS_REPO_MISMATCH` | Check which clone is open; use `import --force` only when importing from the other repository is intentional |
357
- | `CANVAS_TOO_LARGE` | The archive exceeds the 20 MiB import limit |
358
- | `CANVAS_STALE` | The PR head moved; prepare again for the current commit |
359
- | `MODEL_INVALID` | Fix the reported problems in `model.json`, validate, then publish again |
360
- | `SKILL_DIR_EXISTS` | The destination contains a customized directory; preserve it elsewhere before replacing it with `--force` |
361
- | `CHAT_BUSY` | Wait for the running reply or press **stop** |
362
- | `AGENT_AUTH_REQUIRED` | Sign in through the selected agent's CLI, then retry |
363
- | `AGENT_MISSING` or missing chat pane | Check `chat.enabled` and confirm the server can find `acpx` and the selected agent; run `pr-review doctor --all-checks` |
364
- | `AGENT_INCOMPLETE` | Retry the message or increase the chat timeout |
365
- | `COMMENT_FORBIDDEN` | Check the GitHub account's repository access and token permissions |
366
- | `COMMENT_LINE_NOT_IN_DIFF` | Choose a line shown in the current diff |
367
- | `SIGNOFF_INCOMPLETE` | Mark every layer except Other reviewed for this head |
368
- | `FORBIDDEN_HOST` / `CROSS_ORIGIN` | Open the local server using `localhost` or `127.0.0.1` and submit actions from that page |
454
+ | Symptom or code | Next step |
455
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
456
+ | `NOT_A_REPO` | Run inside a Git clone or pass `--repo <dir>` |
457
+ | `NO_ORIGIN` | Check that `origin` points to github.com or GitLab; for self-hosted GitLab set `PR_REVIEW_HOST=gitlab` |
458
+ | `GH_MISSING` / `GH_UNAUTHENTICATED` | Install [GitHub CLI](https://cli.github.com), run `gh auth login`, and check authentication in the same environment that runs the server |
459
+ | `GITHUB_API_ERROR` | Read the underlying error for permissions, rate limits, connectivity, or GitHub service problems |
460
+ | `GLAB_MISSING` / `GLAB_UNAUTHENTICATED` | Install [GitLab CLI](https://gitlab.com/gitlab-org/cli), run `glab auth login`, and check authentication in the same environment that runs the server |
461
+ | `GITLAB_API_ERROR` | Read the underlying error for permissions, rate limits, connectivity, or GitLab service problems |
462
+ | `PR_NOT_FOUND` | Check the PR number, repository, and your access |
463
+ | `CANVAS_NOT_FOUND` | Generate or import a canvas for the requested commit |
464
+ | `CANVAS_INVALID` | Read the format errors; re-export or regenerate the canvas |
465
+ | `CANVAS_REPO_MISMATCH` | Check which clone is open; use `import --force` only when importing from the other repository is intentional |
466
+ | `CANVAS_PR_MISMATCH` | The ZIP was exported for another pull request; import the canvas of this PR, or import that ZIP without `--pr` to store it under its own |
467
+ | `CANVAS_TOO_LARGE` | The archive exceeds the 20 MiB import limit |
468
+ | `CANVAS_STALE` | The PR head moved; prepare again for the current commit |
469
+ | `MODEL_INVALID` | Fix the reported problems in `model.json`, validate, then publish again |
470
+ | `SKILL_DIR_EXISTS` | The destination contains a customized directory; preserve it elsewhere before replacing it with `--force` |
471
+ | `CHAT_BUSY` | Wait for the running reply or press **stop** |
472
+ | `AGENT_AUTH_REQUIRED` | Sign in through the selected agent's CLI, then retry |
473
+ | `AGENT_MISSING` or missing chat pane | Check `chat.enabled` and confirm the server can find `acpx` and the selected agent; run `pr-review doctor --all-checks` |
474
+ | `AGENT_INCOMPLETE` | Retry the message or increase the chat timeout |
475
+ | `COMMENT_FORBIDDEN` | Check the GitHub or GitLab account's repository access and token permissions |
476
+ | `COMMENT_LINE_NOT_IN_DIFF` | Choose a line shown in the current diff |
477
+ | `SIGNOFF_INCOMPLETE` | Mark every layer except Other reviewed for this head |
478
+ | `FORBIDDEN_HOST` / `CROSS_ORIGIN` | Open the local server using `localhost` or `127.0.0.1` and submit actions from that page |
369
479
 
370
480
  ### Validation diagnostics
371
481
 
372
482
  Validation reports name the field, file, hunk, or line to fix. Common groups are:
373
483
 
374
- | Codes | What to check |
375
- |---|---|
376
- | `SCHEMA`, `TEXT_TOO_LONG` | Required fields, types, and text limits |
377
- | `HUNK_UNASSIGNED`, `HUNK_DUPLICATE`, `HUNK_UNKNOWN` | Each known hunk belongs to exactly one layer |
378
- | `PATH_UNKNOWN`, `TEST_PATH_UNKNOWN` | Referenced files exist in the relevant diff or PR head |
379
- | `LAYER_EMPTY`, `LAYER_KEY_DUPLICATE` | Layers contain hunks and have unique keys |
380
- | `OTHER_DUPLICATE`, `OTHER_NOT_LAST`, `RISK_IN_OTHER` | At most one Other layer, last, without risk-tagged changes |
381
- | `TEST_NOT_LAST`, `TEST_IN_OTHER` | Tests follow the code they cover and use the appropriate layer |
382
- | `ANNOTATION_OUTSIDE_HUNK`, `POINT_OUTSIDE_DIFF`, `FOLD_INVALID` | Locations and fold ranges fit the assigned diff |
383
- | `TOO_MANY_POINTS` | Count explicit points and missing-test entries together |
384
- | `LINK_UNRESOLVED`, `DIAGRAM_NODE_UNKNOWN`, `DIAGRAM_LIMIT` | Link targets, diagram node IDs, and diagram counts |
484
+ | Codes | What to check |
485
+ | --------------------------------------------------------------- | -------------------------------------------------------------- |
486
+ | `SCHEMA`, `TEXT_TOO_LONG` | Required fields, types, and text limits |
487
+ | `HUNK_UNASSIGNED`, `HUNK_DUPLICATE`, `HUNK_UNKNOWN` | Each known hunk belongs to exactly one layer |
488
+ | `PATH_UNKNOWN`, `TEST_PATH_UNKNOWN` | Referenced files exist in the relevant diff or PR head |
489
+ | `LAYER_EMPTY`, `LAYER_KEY_DUPLICATE` | Layers contain hunks and have unique keys |
490
+ | `OTHER_DUPLICATE`, `OTHER_NOT_LAST`, `RISK_IN_OTHER` | At most one Other layer, last, without risk-tagged changes |
491
+ | `TEST_NOT_LAST`, `TEST_IN_OTHER` | Tests follow the code they cover and use the appropriate layer |
492
+ | `ANNOTATION_OUTSIDE_HUNK`, `POINT_OUTSIDE_DIFF`, `FOLD_INVALID` | Locations and fold ranges fit the assigned diff |
493
+ | `TOO_MANY_POINTS` | Count explicit points and missing-test entries together |
494
+ | `LINK_UNRESOLVED`, `DIAGRAM_NODE_UNKNOWN`, `DIAGRAM_LIMIT` | Link targets, diagram node IDs, and diagram counts |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vintasoftware/pr-review-canvas",
3
- "version": "0.2.0",
4
- "description": "Localhost PR review canvas: layered diffs, attention points, and GitHub comments for one PR",
3
+ "version": "0.4.0",
4
+ "description": "Localhost PR review canvas: layered diffs, attention points, and GitHub or GitLab comments for one PR",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pr-review": "./bin/pr-review.mjs"
@@ -10,13 +10,25 @@
10
10
  "dev": "tsx watch --clear-screen=false --ignore '**/.pr-review/**' src/cli.ts serve",
11
11
  "start": "tsx src/cli.ts serve",
12
12
  "test": "vitest run",
13
+ "hooks:install": "git config core.hooksPath \"$(git rev-parse --show-toplevel)/.githooks\"",
13
14
  "test:watch": "vitest",
14
15
  "test:browser": "playwright test",
15
16
  "coverage": "vitest run --coverage",
16
17
  "typecheck": "tsc --noEmit",
17
18
  "test:package": "node scripts/test-package.mjs",
18
- "verify": "pnpm typecheck && pnpm coverage && pnpm test:browser --workers=2 && pnpm test:package",
19
- "prepublishOnly": "pnpm verify"
19
+ "verify": "pnpm check && pnpm coverage && pnpm test:browser --workers=2 && pnpm test:package",
20
+ "prepublishOnly": "pnpm verify",
21
+ "lint": "oxlint --deny-warnings",
22
+ "lint:fix": "oxlint --fix --deny-warnings",
23
+ "format": "oxfmt --write",
24
+ "format:check": "oxfmt --check",
25
+ "check": "pnpm lint && pnpm format:check && pnpm typecheck",
26
+ "precommit": "pnpm check && pnpm test",
27
+ "site:dev": "vite --config site/vite.config.js",
28
+ "site:image": "node scripts/render-social-preview.mjs",
29
+ "site:build": "vite build --config site/vite.config.js",
30
+ "site:preview": "vite preview --config site/vite.config.js",
31
+ "site:test": "playwright test --config site/playwright.config.js"
20
32
  },
21
33
  "dependencies": {
22
34
  "@highlightjs/cdn-assets": "^11.12.0",
@@ -36,6 +48,8 @@
36
48
  "@types/node": "^24.13.3",
37
49
  "@vitest/coverage-v8": "^4.1.10",
38
50
  "happy-dom": "20.11.2",
51
+ "oxfmt": "^0.68.0",
52
+ "oxlint": "^1.83.0",
39
53
  "typescript": "^5.9.3",
40
54
  "vite": "~8.0.16",
41
55
  "vitest": "^4.1.10"
@@ -30,10 +30,10 @@ highRisk:
30
30
  label: auth
31
31
 
32
32
  generation:
33
- mode: strict # strict: quality review; surfacing: walkthrough that audits as it reads
34
- maxRepairRounds: 3 # publish retries the skill may make after a failed validation
33
+ mode: strict # strict: quality review; surfacing: walkthrough that audits as it reads
34
+ maxRepairRounds: 3 # publish retries the skill may make after a failed validation
35
35
  inlineDiffMaxLines: 1500 # diffs longer than this are read per file instead of inlined in the prompt
36
- smallPrHunks: 10 # at most this many hunks: one layer unless concerns differ, fewer annotations
36
+ smallPrHunks: 10 # at most this many hunks: one layer unless concerns differ, fewer annotations
37
37
  # caps: # override the text caps (characters)
38
38
  # summary: 1800
39
39
  # rationale: 300
@@ -46,4 +46,10 @@ tests:
46
46
  patterns: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**']
47
47
 
48
48
  chat:
49
- enabled: true # false removes the chat pane and its routes
49
+ enabled: true # false removes the chat pane and its routes
50
+
51
+ canvas:
52
+ # A canvas stays current for a later head whose diff is identical to the one it was generated
53
+ # from, as after merging the base branch in. Set false to mark the canvas outdated on every new
54
+ # commit.
55
+ keepForIdenticalDiff: true
@@ -11,6 +11,9 @@ At most six sentences. No headings unless the reader asks for more. Markdown is
11
11
  short lists, `path:line` references. Do not restate the question. Do not summarize the whole PR
12
12
  when the reader asked about one file.
13
13
 
14
+ Call diff sections **chunks** in answers and proposed comments. Keep code identifiers and
15
+ `#hunk:` link targets unchanged.
16
+
14
17
  ## Answer protocol
15
18
 
16
19
  For any "is this fine / covered / needed / safe?" question, the verdict comes first, in one of
@@ -39,6 +39,9 @@ hunk in that file's patch.
39
39
 
40
40
  ## Layering rules
41
41
 
42
+ In reader-facing prose, call diff sections **chunks**. Keep the schema field `hunks` and
43
+ `#hunk:` link targets exactly as specified.
44
+
42
45
  Project-configured layers (optional guidance):
43
46
 
44
47
  {{CONFIGURED_LAYERS}}