fix(review): language-tagged suggestion fence for Gitea syntax highlighting
Gitea 1.26.x has no GitHub-style 'Apply suggestion' button — a ```suggestion fence is just an unknown-language code block, so chroma does not highlight it and there is no apply control. Switch inline_comment_body to wrap the suggested fix in a fence tagged with the file's language (new _lang_for_path helper, .java→java, .ts→typescript, .py→python, ...), so Gitea syntax-highlights the code. No capability lost (there was never an apply button on this Gitea version). Correct the docstrings/skills/README that wrongly claimed an apply-button was rendered. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -6,7 +6,7 @@ checked-out copy of the target repo at the PR head sha, then `opencode run` is
|
|||||||
launched there. The `pragent` primary agent reviews the diff with real tools
|
launched there. The `pragent` primary agent reviews the diff with real tools
|
||||||
(subagents, LSP/linters via bash, webfetch references) and emits a structured
|
(subagents, LSP/linters via bash, webfetch references) and emits a structured
|
||||||
findings JSON. A thin Python shell posts that JSON back to Gitea as inline
|
findings JSON. A thin Python shell posts that JSON back to Gitea as inline
|
||||||
comments + ```suggestion blocks + a summary (dedupe + anchor validation stay
|
comments + language-highlighted suggested-fix blocks + a summary (dedupe + anchor validation stay
|
||||||
deterministic in Python).
|
deterministic in Python).
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
@@ -40,7 +40,7 @@ flowchart TD
|
|||||||
PR --> JSON["final message: summary + ```json findings```"]
|
PR --> JSON["final message: summary + ```json findings```"]
|
||||||
JSON --> PARSE["ai_review.parse_review_output<br/>{summary, findings}"]
|
JSON --> PARSE["ai_review.parse_review_output<br/>{summary, findings}"]
|
||||||
PARSE --> ANCHOR["parse_diff_anchors → split_findings"]
|
PARSE --> ANCHOR["parse_diff_anchors → split_findings"]
|
||||||
ANCHOR --> POST["post_inline_review<br/>summary + inline ```suggestion + ref links + sha marker"]
|
ANCHOR --> POST["post_inline_review<br/>summary + inline lang-tagged fix block + ref links + sha marker"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lean by default
|
## Lean by default
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ read the full file around a flagged line, not just the diff hunk.
|
|||||||
the POST-CHANGE version of `path` — a context line or an added `+` line shown
|
the POST-CHANGE version of `path` — a context line or an added `+` line shown
|
||||||
in the diff. Never a removed line. If unsure, use the closest context line you
|
in the diff. Never a removed line. If unsure, use the closest context line you
|
||||||
can see in the diff. A finding with a bad line gets folded into the summary as
|
can see in the diff. A finding with a bad line gets folded into the summary as
|
||||||
a bullet instead of an inline comment, so anchoring correctly is what makes a
|
a bullet instead of an inline comment, so anchoring correctly is what gets a
|
||||||
suggestion apply-able in Gitea.
|
finding shown inline with its suggested-fix code block (language-highlighted).
|
||||||
|
|
||||||
## Output — REQUIRED exact shape
|
## Output — REQUIRED exact shape
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,10 @@ message — so the JSON must be the last thing, and it must be valid.
|
|||||||
- `suggestion` — the literal new code replacing the flagged line(s). Minimal,
|
- `suggestion` — the literal new code replacing the flagged line(s). Minimal,
|
||||||
just the changed lines, indented as they appear in the file. **Empty string**
|
just the changed lines, indented as they appear in the file. **Empty string**
|
||||||
when no safe textual replacement exists (missing test, architectural note, a
|
when no safe textual replacement exists (missing test, architectural note, a
|
||||||
fix that needs context beyond one hunk). This is wrapped in a ```suggestion
|
fix that needs context beyond one hunk). The pilot wraps this in a fenced code
|
||||||
fence → Gitea renders an **apply button**.
|
block tagged with the file's language so Gitea syntax-highlights it. (Gitea
|
||||||
|
1.26.x has no GitHub-style "Apply suggestion" button, so a ```suggestion fence
|
||||||
|
is not used — a language-tagged fence gives highlighting and loses nothing.)
|
||||||
- `reference` — a URL (CVE, library docs, spec) backing the finding, or `""`.
|
- `reference` — a URL (CVE, library docs, spec) backing the finding, or `""`.
|
||||||
Only link authoritative sources; don't fabricate URLs.
|
Only link authoritative sources; don't fabricate URLs.
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ Each finding's `line` MUST be a line that exists in the POST-CHANGE version of
|
|||||||
Never anchor on a **removed** (`-`) line — it has no post-change line number.
|
Never anchor on a **removed** (`-`) line — it has no post-change line number.
|
||||||
If you're unsure of the exact line, use the closest context line you CAN see in
|
If you're unsure of the exact line, use the closest context line you CAN see in
|
||||||
the diff. A misanchored finding becomes a summary bullet instead of an inline
|
the diff. A misanchored finding becomes a summary bullet instead of an inline
|
||||||
comment, so correct anchoring is what makes a ```suggestion apply-able in Gitea.
|
comment, so correct anchoring is what gets a finding shown inline with its
|
||||||
|
suggested-fix code block (language-highlighted) rather than demoted to a bullet.
|
||||||
|
|
||||||
## Honoring repo config
|
## Honoring repo config
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,14 @@ ai_review.review_pr() (same core the CI-step uses)
|
|||||||
7. post review → POST .../pulls/{i}/reviews (event: COMMENT) as pragent-bot
|
7. post review → POST .../pulls/{i}/reviews (event: COMMENT) as pragent-bot
|
||||||
- prose summary → review body intro
|
- prose summary → review body intro
|
||||||
- anchored findings → inline line comments, body wraps `suggestion` in a
|
- anchored findings → inline line comments, body wraps `suggestion` in a
|
||||||
```suggestion fence (Gitea renders an apply-button); reference → 📎 ref link
|
language-tagged fenced code block (Gitea syntax-highlights it; Gitea
|
||||||
|
1.26.x has no apply-suggestion button); reference → 📎 ref link
|
||||||
- unanchored findings → summary-body bullets
|
- unanchored findings → summary-body bullets
|
||||||
- summary body carries the <!-- pragent:sha=... --> marker for dedupe
|
- summary body carries the <!-- pragent:sha=... --> marker for dedupe
|
||||||
```
|
```
|
||||||
|
|
||||||
Fail-open. No duplicate per commit (dedupe). Inline comments + apply-able
|
Fail-open. No duplicate per commit (dedupe). Inline comments + syntax-highlighted
|
||||||
suggestions where the line anchors cleanly. Repo-local focus via
|
suggested-fix blocks where the line anchors cleanly. Repo-local focus via
|
||||||
`.pr-review.json`. Prior reviews fed as context so re-pushes synthesize instead
|
`.pr-review.json`. Prior reviews fed as context so re-pushes synthesize instead
|
||||||
of repeating (light version of framework §6.1).
|
of repeating (light version of framework §6.1).
|
||||||
|
|
||||||
|
|||||||
+47
-11
@@ -5,8 +5,9 @@ Runs as a Gitea Actions step OR is called by the central webhook server
|
|||||||
(`webhook_server.py`). Fetches a PR diff, asks glm-5.2:cloud (via the on-network
|
(`webhook_server.py`). Fetches a PR diff, asks glm-5.2:cloud (via the on-network
|
||||||
headroom proxy, Anthropic /v1/messages format) to review it, and posts the
|
headroom proxy, Anthropic /v1/messages format) to review it, and posts the
|
||||||
findings back as `pragent-bot` — as a **review summary** plus **inline line
|
findings back as `pragent-bot` — as a **review summary** plus **inline line
|
||||||
comments** with apply-able ```suggestion blocks where the model could produce
|
comments** with a fenced suggested-fix block (tagged with the file's language so
|
||||||
them and the line anchors cleanly to the post-change file.
|
Gitea syntax-highlights it) where the model could produce one and the line
|
||||||
|
anchors cleanly to the post-change file.
|
||||||
|
|
||||||
Features (pilot v2):
|
Features (pilot v2):
|
||||||
- **Dedupe / persistence:** Gitea itself is the source of truth. Before
|
- **Dedupe / persistence:** Gitea itself is the source of truth. Before
|
||||||
@@ -22,9 +23,12 @@ Features (pilot v2):
|
|||||||
findings with `path`/`line`. We parse the diff hunks to learn which
|
findings with `path`/`line`. We parse the diff hunks to learn which
|
||||||
`(path, new_line)` pairs are valid post-change anchors and post each
|
`(path, new_line)` pairs are valid post-change anchors and post each
|
||||||
anchored finding as a positional review comment; the `suggestion` field, if
|
anchored finding as a positional review comment; the `suggestion` field, if
|
||||||
non-empty, is wrapped in a ```suggestion fence so Gitea renders an
|
non-empty, is wrapped in a fenced code block tagged with the file's language
|
||||||
apply-button. Findings that don't anchor (bad line, unchanged file, etc.)
|
(via `_lang_for_path`) so Gitea syntax-highlights it. Gitea 1.26.x has no
|
||||||
are folded into the summary body as plain bullets.
|
GitHub-style "Apply suggestion" button, so a language-tagged block is used
|
||||||
|
for highlighting instead of a ```suggestion fence. Findings that don't
|
||||||
|
anchor (bad line, unchanged file, etc.) are folded into the summary body as
|
||||||
|
plain bullets.
|
||||||
|
|
||||||
Fail-open by design: any error becomes a short "review failed" review comment,
|
Fail-open by design: any error becomes a short "review failed" review comment,
|
||||||
and review_pr never raises. Stdlib only — no pip install.
|
and review_pr never raises. Stdlib only — no pip install.
|
||||||
@@ -431,19 +435,51 @@ def split_findings(findings: list[dict], anchors: dict[str, set[int]]) -> tuple[
|
|||||||
return anchored, unanchored
|
return anchored, unanchored
|
||||||
|
|
||||||
|
|
||||||
|
def _lang_for_path(path: str) -> str:
|
||||||
|
"""Map a file extension to a chroma language tag for fenced code blocks.
|
||||||
|
|
||||||
|
Used so the suggested-fix block is syntax-highlighted in Gitea. Gitea 1.26.x
|
||||||
|
has no GitHub-style "Apply suggestion" button (the ```suggestion fence is
|
||||||
|
just an unknown-language code block → plain monospace, no apply), so we tag
|
||||||
|
the block with the file's real language for highlighting instead.
|
||||||
|
"""
|
||||||
|
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
|
||||||
|
return {
|
||||||
|
"java": "java", "kt": "kotlin", "scala": "scala", "groovy": "groovy",
|
||||||
|
"ts": "typescript", "tsx": "tsx", "js": "javascript", "jsx": "jsx",
|
||||||
|
"mjs": "javascript", "cjs": "javascript",
|
||||||
|
"py": "python", "pyi": "python",
|
||||||
|
"go": "go", "rs": "rust", "rb": "ruby", "php": "php",
|
||||||
|
"c": "c", "h": "c", "cpp": "cpp", "cc": "cpp", "hpp": "cpp",
|
||||||
|
"cs": "csharp", "swift": "swift", "m": "objc",
|
||||||
|
"sh": "bash", "bash": "bash", "zsh": "bash",
|
||||||
|
"yml": "yaml", "yaml": "yaml", "json": "json", "jsonc": "json",
|
||||||
|
"toml": "toml", "ini": "ini", "cfg": "ini",
|
||||||
|
"html": "html", "htm": "html", "css": "css", "scss": "scss",
|
||||||
|
"xml": "xml", "svg": "xml", "sql": "sql",
|
||||||
|
"md": "markdown", "dockerfile": "dockerfile",
|
||||||
|
}.get(ext, "")
|
||||||
|
|
||||||
|
|
||||||
def inline_comment_body(f: dict) -> str:
|
def inline_comment_body(f: dict) -> str:
|
||||||
"""Render one finding as a positional review-comment body.
|
"""Render one finding as a positional review-comment body.
|
||||||
|
|
||||||
Includes a ```suggestion fence only if the model produced non-empty
|
Includes a fenced suggested-fix block only if the model produced non-empty
|
||||||
replacement code. Gitea renders that as an apply-able suggestion. Appends a
|
replacement code. The fence is tagged with the file's language (via
|
||||||
`📎 ref:` link when the finding carries a `reference` URL.
|
`_lang_for_path`) so Gitea syntax-highlights it — Gitea 1.26.x has no
|
||||||
|
GitHub-style "Apply suggestion" button (```suggestion is just an
|
||||||
|
unknown-language block there → plain monospace), so a language-tagged block
|
||||||
|
is strictly more readable and loses nothing. Appends a `📎 ref:` link when
|
||||||
|
the finding carries a `reference` URL.
|
||||||
"""
|
"""
|
||||||
sev = f["severity"].upper()
|
sev = f["severity"].upper()
|
||||||
body = f"**[{sev}]** {f['problem']}"
|
body = f"**[{sev}]** {f['problem']}"
|
||||||
if f["fix"]:
|
if f["fix"]:
|
||||||
body += f"\n\nFix: {f['fix']}"
|
body += f"\n\nFix: {f['fix']}"
|
||||||
if f["suggestion"]:
|
if f["suggestion"]:
|
||||||
body += f"\n\n```suggestion\n{f['suggestion']}\n```"
|
lang = _lang_for_path(f.get("path", ""))
|
||||||
|
fence = f"```{lang}" if lang else "```"
|
||||||
|
body += f"\n\n{fence}\n{f['suggestion']}\n```"
|
||||||
ref = f.get("reference", "")
|
ref = f.get("reference", "")
|
||||||
if ref:
|
if ref:
|
||||||
body += f"\n\n📎 ref: {ref}"
|
body += f"\n\n📎 ref: {ref}"
|
||||||
@@ -632,8 +668,8 @@ def post_inline_review(
|
|||||||
honored here and silently leave the comment unpositioned (Gitea then renders
|
honored here and silently leave the comment unpositioned (Gitea then renders
|
||||||
a file-level comment on EVERY diff line of the file, which is the flood we
|
a file-level comment on EVERY diff line of the file, which is the flood we
|
||||||
hit). `f["line"]` is already a validated post-change (RIGHT-side) line from
|
hit). `f["line"]` is already a validated post-change (RIGHT-side) line from
|
||||||
`split_findings`, so it maps directly to `new_position`. The body carries the
|
`split_findings`, so it maps directly to `new_position`. The body carries a
|
||||||
```suggestion fence when the model produced replacement code.
|
language-tagged fenced code block when the model produced replacement code.
|
||||||
"""
|
"""
|
||||||
comments = [
|
comments = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -268,14 +268,23 @@ def test_inline_comment_body_with_suggestion():
|
|||||||
body = inline_comment_body(f)
|
body = inline_comment_body(f)
|
||||||
assert "**[HIGH]**" in body
|
assert "**[HIGH]**" in body
|
||||||
assert "bad" in body
|
assert "bad" in body
|
||||||
assert "```suggestion\n" in body
|
# no extension → bare fence (Gitea 1.26.x has no apply-suggestion; we tag
|
||||||
|
# with the file language for highlighting instead of ```suggestion)
|
||||||
|
assert "```\ngood()\n```" in body
|
||||||
assert "good()" in body
|
assert "good()" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_inline_comment_body_suggestion_lang_tagged():
|
||||||
|
f = {"severity": "high", "path": "src/Foo.java", "line": 1,
|
||||||
|
"problem": "bad", "fix": "swap", "suggestion": "good();"}
|
||||||
|
body = inline_comment_body(f)
|
||||||
|
assert "```java\ngood();\n```" in body
|
||||||
|
|
||||||
|
|
||||||
def test_inline_comment_body_no_suggestion():
|
def test_inline_comment_body_no_suggestion():
|
||||||
f = {"severity": "low", "path": "a", "line": 1, "problem": "p", "fix": "f", "suggestion": ""}
|
f = {"severity": "low", "path": "a", "line": 1, "problem": "p", "fix": "f", "suggestion": ""}
|
||||||
body = inline_comment_body(f)
|
body = inline_comment_body(f)
|
||||||
assert "```suggestion" not in body
|
assert "```" not in body
|
||||||
assert "Fix: f" in body
|
assert "Fix: f" in body
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user