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
|
||||
(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
|
||||
comments + ```suggestion blocks + a summary (dedupe + anchor validation stay
|
||||
comments + language-highlighted suggested-fix blocks + a summary (dedupe + anchor validation stay
|
||||
deterministic in Python).
|
||||
|
||||
## Layout
|
||||
@@ -40,7 +40,7 @@ flowchart TD
|
||||
PR --> JSON["final message: summary + ```json findings```"]
|
||||
JSON --> PARSE["ai_review.parse_review_output<br/>{summary, 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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
a bullet instead of an inline comment, so anchoring correctly is what makes a
|
||||
suggestion apply-able in Gitea.
|
||||
a bullet instead of an inline comment, so anchoring correctly is what gets a
|
||||
finding shown inline with its suggested-fix code block (language-highlighted).
|
||||
|
||||
## 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,
|
||||
just the changed lines, indented as they appear in the file. **Empty string**
|
||||
when no safe textual replacement exists (missing test, architectural note, a
|
||||
fix that needs context beyond one hunk). This is wrapped in a ```suggestion
|
||||
fence → Gitea renders an **apply button**.
|
||||
fix that needs context beyond one hunk). The pilot wraps this in a fenced code
|
||||
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 `""`.
|
||||
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.
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
- prose summary → review body intro
|
||||
- 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
|
||||
- summary body carries the <!-- pragent:sha=... --> marker for dedupe
|
||||
```
|
||||
|
||||
Fail-open. No duplicate per commit (dedupe). Inline comments + apply-able
|
||||
suggestions where the line anchors cleanly. Repo-local focus via
|
||||
Fail-open. No duplicate per commit (dedupe). Inline comments + syntax-highlighted
|
||||
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
|
||||
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
|
||||
headroom proxy, Anthropic /v1/messages format) to review it, and posts the
|
||||
findings back as `pragent-bot` — as a **review summary** plus **inline line
|
||||
comments** with apply-able ```suggestion blocks where the model could produce
|
||||
them and the line anchors cleanly to the post-change file.
|
||||
comments** with a fenced suggested-fix block (tagged with the file's language so
|
||||
Gitea syntax-highlights it) where the model could produce one and the line
|
||||
anchors cleanly to the post-change file.
|
||||
|
||||
Features (pilot v2):
|
||||
- **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
|
||||
`(path, new_line)` pairs are valid post-change anchors and post each
|
||||
anchored finding as a positional review comment; the `suggestion` field, if
|
||||
non-empty, is wrapped in a ```suggestion fence so Gitea renders an
|
||||
apply-button. Findings that don't anchor (bad line, unchanged file, etc.)
|
||||
are folded into the summary body as plain bullets.
|
||||
non-empty, is wrapped in a fenced code block tagged with the file's language
|
||||
(via `_lang_for_path`) so Gitea syntax-highlights it. Gitea 1.26.x has no
|
||||
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,
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""Render one finding as a positional review-comment body.
|
||||
|
||||
Includes a ```suggestion fence only if the model produced non-empty
|
||||
replacement code. Gitea renders that as an apply-able suggestion. Appends a
|
||||
`📎 ref:` link when the finding carries a `reference` URL.
|
||||
Includes a fenced suggested-fix block only if the model produced non-empty
|
||||
replacement code. The fence is tagged with the file's language (via
|
||||
`_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()
|
||||
body = f"**[{sev}]** {f['problem']}"
|
||||
if f["fix"]:
|
||||
body += f"\n\nFix: {f['fix']}"
|
||||
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", "")
|
||||
if 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
|
||||
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
|
||||
`split_findings`, so it maps directly to `new_position`. The body carries the
|
||||
```suggestion fence when the model produced replacement code.
|
||||
`split_findings`, so it maps directly to `new_position`. The body carries a
|
||||
language-tagged fenced code block when the model produced replacement code.
|
||||
"""
|
||||
comments = [
|
||||
{
|
||||
|
||||
@@ -268,14 +268,23 @@ def test_inline_comment_body_with_suggestion():
|
||||
body = inline_comment_body(f)
|
||||
assert "**[HIGH]**" 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
|
||||
|
||||
|
||||
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():
|
||||
f = {"severity": "low", "path": "a", "line": 1, "problem": "p", "fix": "f", "suggestion": ""}
|
||||
body = inline_comment_body(f)
|
||||
assert "```suggestion" not in body
|
||||
assert "```" not in body
|
||||
assert "Fix: f" in body
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user