pr-review-canvas

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
Read pstack runtime before executing this workflow.
在执行此工作流前,请阅读 pstack runtime。

PR Review Canvas

PR Review Canvas

Generate an interactive HTML review of a GitHub PR that reads like a peer walking you through what matters.
生成一份交互式 GitHub PR HTML 审查报告,阅读体验就像一位同事带着你逐步了解真正重要的变更。

Workflow

工作流

1. Fetch PR data

1. 获取 PR 数据

Run these
gh api
calls in parallel:
bash
gh api repos/{owner}/{repo}/pulls/{number} --jq '{title, body, user: .user.login, state, additions, deletions, changed_files, base: .base.ref, head: .head.ref}'
gh api repos/{owner}/{repo}/pulls/{number}/files --paginate --jq '.[] | {filename, status, additions, deletions, patch}'
gh api repos/{owner}/{repo}/pulls/{number}/comments --jq '.[] | {user: .user.login, body, path, line}'
并行运行以下
gh api
调用:
bash
gh api repos/{owner}/{repo}/pulls/{number} --jq '{title, body, user: .user.login, state, additions, deletions, changed_files, base: .base.ref, head: .head.ref}'
gh api repos/{owner}/{repo}/pulls/{number}/files --paginate --jq '.[] | {filename, status, additions, deletions, patch}'
gh api repos/{owner}/{repo}/pulls/{number}/comments --jq '.[] | {user: .user.login, body, path, line}'

2. Analyze the PR and write the body HTML

2. 分析 PR 并编写 body HTML

Read the diffs, understand the PR, and write the
<body>
content directly as HTML. You have full creative freedom -- the goal is to explain the PR clearly to a reviewer. Use whatever structure best fits the PR.
Typical structure (adapt as needed):
  • Header with title, PR number, author, stats
  • Summary box explaining what the PR does in plain English
  • Core file sections with annotations and diffs
  • Mechanical/boilerplate files collapsed by default
  • Review checklist at the bottom
But you can also add:
  • Pseudocode summaries for verbose code -- show the algorithm in plain English or short pseudocode, with the real diff collapsed below (use a
    .bp-section
    card labeled "Show full implementation"). Great when 150 lines of retry/backoff/error-handling code is really just "fetch with exponential backoff and circuit breaker."
  • Diagrams (inline SVG, mermaid via CDN, ASCII art in
    <pre>
    )
  • Flowcharts showing before/after control flow
  • Tables comparing old vs new behavior
  • Callout boxes for warnings, questions, or gotchas
  • Interactive widgets if they help
  • Anything else that makes the review clearer
Pseudocode pattern example:
html
<div class="file-card">
  <div class="file-hdr" onclick="toggle(this)">
    <span class="fname">retryClient.ts</span>
    <div class="fstats"><span class="pill add">+173</span><span class="pill del">&minus;11</span><span class="chev open">&#9654;</span></div>
  </div>
  <div class="file-body open">
    <div class="file-note">
      <strong>What this does in plain English:</strong>
      <pre style="margin-top:8px;color:var(--text);font-size:12px;line-height:1.6;">
fetch(url):
  if circuit breaker is open → fail fast
  retry up to N times:
    try fetch with timeout
    on success → close circuit breaker, return
    on retryable error → wait (exponential backoff + jitter)
    on non-retryable error → throw
  circuit breaker records failure</pre>
    </div>
    <div class="bp-section" style="margin:0;border:0;border-radius:0;">
      <div class="bp-hdr" onclick="toggleBP(this)">
        <span>Show full implementation (+173 lines)</span><span class="chev">&#9654;</span>
      </div>
      <div class="bp-body"><div data-diff="retryClient"></div></div>
    </div>
  </div>
</div>
阅读 diff,理解 PR,并直接将
<body>
内容编写为 HTML。你可以完全自由发挥——目标是向审查者清晰解释这个 PR。请使用最适合该 PR 的结构。
典型结构(根据需要调整):
  • 包含标题、PR 编号、作者和统计信息的标题栏
  • 用通俗英语解释该 PR 功能的摘要框
  • 带注释和 diff 的核心文件章节
  • 默认折叠的机械性改动/样板代码文件
  • 位于底部的审查清单
还可以添加:
  • 针对冗长代码的伪代码摘要——用通俗英语或简短伪代码展示算法,并将实际 diff 折叠在下方(使用标有 “Show full implementation” 的
    .bp-section
    卡片)。当 150 行重试/退避/错误处理代码本质上只是“使用指数退避和熔断器执行 fetch”时,这种方式尤其合适。
  • 图表(内联 SVG、通过 CDN 引入的 mermaid、
    <pre>
    中的 ASCII art)
  • 展示变更前后控制流的流程图
  • 对比新旧行为的表格
  • 用于呈现警告、问题或容易踩坑之处的提示框
  • 有助于理解的交互式组件
  • 任何其他能让审查更清晰的内容
伪代码模式示例:
html
<div class="file-card">
  <div class="file-hdr" onclick="toggle(this)">
    <span class="fname">retryClient.ts</span>
    <div class="fstats"><span class="pill add">+173</span><span class="pill del">&minus;11</span><span class="chev open">&#9654;</span></div>
  </div>
  <div class="file-body open">
    <div class="file-note">
      <strong>What this does in plain English:</strong>
      <pre style="margin-top:8px;color:var(--text);font-size:12px;line-height:1.6;">
fetch(url):
  if circuit breaker is open → fail fast
  retry up to N times:
    try fetch with timeout
    on success → close circuit breaker, return
    on retryable error → wait (exponential backoff + jitter)
    on non-retryable error → throw
  circuit breaker records failure</pre>
    </div>
    <div class="bp-section" style="margin:0;border:0;border-radius:0;">
      <div class="bp-hdr" onclick="toggleBP(this)">
        <span>Show full implementation (+173 lines)</span><span class="chev">&#9654;</span>
      </div>
      <div class="bp-body"><div data-diff="retryClient"></div></div>
    </div>
  </div>
</div>

3. Available CSS classes and JS utilities

3. 可用的 CSS 类和 JS 工具函数

Read styles.css and renderer.js from this skill directory. These give you a prebuilt dark-themed toolkit. Inject them into template.html verbatim.
CSS classes you can use:
ClassPurpose
.header
,
.header h1
,
.header-meta
Page header
.pill.add
,
.pill.del
,
.pill.files
Stat badges (+N, -N, N files)
.content
Centered content wrapper (max 900px)
.summary
Summary/TL;DR box
.section-title
Section heading with bottom border
.ic
Inline code reference (mono, blue, dark bg)
.file-card
,
.file-hdr
,
.file-body
Collapsible file card (use
onclick="toggle(this)"
on
.file-hdr
)
.file-note
Sticky reviewer annotation inside a file card
.bp-section
,
.bp-hdr
,
.bp-body
Collapsed boilerplate card (use
onclick="toggleBP(this)"
)
.bp-note
Note inside a boilerplate card
.verdict
Review checklist box
JS functions available:
FunctionUsage
toggle(hdrElement)
Toggle a
.file-body
open/closed
toggleBP(hdrElement)
Toggle a
.bp-body
open/closed
renderDiff(target, diffInput)
Render a unified diff.
target
can be a DOM element, string ID, or CSS selector.
diffInput
can be a raw patch string OR an array of lines -- both work. Automatically filters imports, collapses whitespace-only changes, detects moved code (blue/purple tint).
esc(string)
HTML-escape a string
Rendering diffs -- use
data-diff
attributes with auto-discovery.
Put
<div data-diff="KEY"></div>
placeholders in your body HTML wherever you want a diff rendered. The renderer finds them automatically after DOM load and fills them from the
<script id="pr-diffs-json" type="application/json">
element in
template.html
.
CRITICAL: Patch strings can contain
</script>
in addition to newlines, backslashes, and quotes.
Even
json.dumps(...)
is not enough if you paste raw output into executable
<script>
because HTML parsing can terminate the tag early. Never manually embed patch strings in JS/JSON. Instead, use this safe approach:
  1. During the fetch step, save patches to a JSON file using
    jq
    (which handles escaping correctly):
bash
gh api repos/{owner}/{repo}/pulls/{number}/files --paginate \
  --jq '[.[] | {key: (.filename | gsub("[^a-zA-Z0-9]"; "_")), value: (.patch // "")}] | from_entries' \
  > /tmp/pr-patches-{number}.json
  1. During assembly, use Python to safely inject the JSON into
    template.html
    :
bash
python3 <<'PY'
import json
from pathlib import Path

patches = json.loads(Path('/tmp/pr-patches-{number}.json').read_text())
html = Path('/tmp/pr-review-{number}-body.html').read_text()
css = Path('styles.css').read_text()
js = Path('renderer.js').read_text()
tmpl = Path('template.html').read_text()
从该 skill 目录中读取 styles.css 和 renderer.js。它们提供了一套预置的深色主题工具包。将它们原样注入 template.html。
可用的 CSS 类:
ClassPurpose
.header
,
.header h1
,
.header-meta
页面标题栏
.pill.add
,
.pill.del
,
.pill.files
统计徽章(+N、-N、N 个文件)
.content
居中内容容器(最大宽度 900px)
.summary
摘要/TL;DR 框
.section-title
带底部边框的章节标题
.ic
行内代码引用(等宽字体、蓝色、深色背景)
.file-card
,
.file-hdr
,
.file-body
可折叠文件卡片(在
.file-hdr
上使用
onclick="toggle(this)"
)
.file-note
文件卡片内的粘性审查者注释
.bp-section
,
.bp-hdr
,
.bp-body
折叠的样板代码卡片(使用
onclick="toggleBP(this)"
)
.bp-note
样板代码卡片内的注释
.verdict
审查清单框
可用的 JS 函数:
FunctionUsage
toggle(hdrElement)
切换
.file-body
的展开/折叠状态
toggleBP(hdrElement)
切换
.bp-body
的展开/折叠状态
renderDiff(target, diffInput)
渲染 unified diff。
target
可以是 DOM 元素、字符串 ID 或 CSS selector。
diffInput
可以是原始 patch 字符串或行数组——两者均可。该函数会自动过滤 import、折叠仅含空白符的改动,并检测移动的代码(以蓝色/紫色显示)。
esc(string)
对字符串进行 HTML 转义
渲染 diff——使用带自动发现功能的
data-diff
属性。
在 body HTML 中希望显示 diff 的位置放入
<div data-diff="KEY"></div>
占位符。页面 DOM 加载后,renderer 会自动找到这些占位符,并使用 template.html 中
<script id="pr-diffs-json" type="application/json">
元素的内容填充它们。
关键:patch 字符串除了换行符、反斜杠和引号外,还可能包含
</script>
。
即使
json.dumps(...)
也不够用;如果将原始输出粘贴到可执行的
<script>
中,HTML 解析器可能会提前终止该标签。切勿在 JS/JSON 中手动嵌入 patch 字符串。请改用以下安全方式:
  1. 在 fetch 步骤中,使用
    jq
    (可正确处理转义)将 patches 保存为 JSON 文件:
bash
gh api repos/{owner}/{repo}/pulls/{number}/files --paginate \
  --jq '[.[] | {key: (.filename | gsub("[^a-zA-Z0-9]"; "_")), value: (.patch // "")}] | from_entries' \
  > /tmp/pr-patches-{number}.json
  1. 在组装步骤中,使用 Python 将 JSON 安全注入 template.html:
bash
python3 <<'PY'
import json
from pathlib import Path

patches = json.loads(Path('/tmp/pr-patches-{number}.json').read_text())
html = Path('/tmp/pr-review-{number}-body.html').read_text()
css = Path('styles.css').read_text()
js = Path('renderer.js').read_text()
tmpl = Path('template.html').read_text()

Prevent literal </script> from terminating HTML script tags early.

Prevent literal </script> from terminating HTML script tags early.

safe_json = json.dumps(patches).replace('<', '\u003c').replace('>', '\u003e').replace('&', '\u0026')
out = ( tmpl.replace('/* INJECT_CSS /', css) .replace('/ INJECT_JS */', js) .replace('<!-- INJECT_BODY -->', html) .replace('{"PR_DIFFS_PLACEHOLDER":true}', safe_json) )
Path('/tmp/pr-review-{number}.html').write_text(out) PY

This guarantees valid JSON and script-safe HTML embedding. The agent writes body HTML to a temp file, then Python assembles everything safely.

The diff data keys should match the `data-diff` attribute values in the HTML:
```html
<div data-diff="path_to_file_ts"></div>
Since renderer.js loads in
<head>
, you can also call
renderDiff(target, lines)
directly from inline
<script>
tags if needed for custom use cases. The function accepts a DOM element, ID string, or CSS selector as
target
, and a string or array as
lines
.
You're not limited to these. Add your own inline
<style>
blocks,
<script>
blocks, SVGs, diagrams, or anything else. The prebuilt pieces save time but don't constrain you.
safe_json = json.dumps(patches).replace('<', '\u003c').replace('>', '\u003e').replace('&', '\u0026')
out = ( tmpl.replace('/* INJECT_CSS /', css) .replace('/ INJECT_JS */', js) .replace('<!-- INJECT_BODY -->', html) .replace('{"PR_DIFFS_PLACEHOLDER":true}', safe_json) )
Path('/tmp/pr-review-{number}.html').write_text(out) PY

这能保证生成有效的 JSON,并实现对 script 安全的 HTML 嵌入。Agent 会将 body HTML 写入临时文件,然后由 Python 安全地组装所有内容。

diff 数据的键应与 body HTML 中 `data-diff` 属性的值匹配:
```html
<div data-diff="path_to_file_ts"></div>
由于 renderer.js 加载于
<head>
中,如需支持自定义用例,也可以直接从内联
<script>
标签中调用
renderDiff(target, lines)
。
该函数接受 DOM 元素、ID 字符串或 CSS selector 作为
target
,并接受字符串或数组作为
lines
。
你并不局限于上述内容。 可以添加自己的内联
<style>
块、
<script>
块、SVG、图表或任何其他内容。预置组件可以节省时间,但不会限制你的发挥。

4. Assemble and serve

4. 组装并提供服务

  1. Write your body HTML (everything that goes inside
    <body>
    ) to
    /tmp/pr-review-{number}-body.html
  2. Save patches to
    /tmp/pr-patches-{number}.json
    using the
    jq
    command from step 3 above
  3. Run the Python assembly script from step 3 above (reads styles.css, renderer.js, template.html from this skill directory, injects body + patches safely, writes final HTML)
  4. Start a local server on a fixed port:
    bash
    cd /tmp && python3 -m http.server 8432 --bind 127.0.0.1
    Run this backgrounded, then navigate the in-app browser to
    http://127.0.0.1:8432/pr-review-{number}.html
    .
    Why a fixed port and
    cd /tmp
    :
    Background shells have no TTY, so Python buffers its startup message ("Serving HTTP on...") indefinitely — using port 0 means you can never read which port was chosen. And
    --directory /tmp
    works but
    cd /tmp
    is more robust across Python versions. If port 8432 is taken, try 8433, 8434, etc.
  1. 将 body HTML(
    <body>
    内的全部内容)写入
    /tmp/pr-review-{number}-body.html
  2. 使用第 3 步中的
    jq
    命令,将 patches 保存到
    /tmp/pr-patches-{number}.json
  3. 运行第 3 步中的 Python 组装脚本(从该 skill 目录读取 styles.css、renderer.js 和 template.html,安全注入 body + patches,并写出最终 HTML)
  4. 在固定端口上启动本地服务器:
    bash
    cd /tmp && python3 -m http.server 8432 --bind 127.0.0.1
    在后台运行它,然后让应用内浏览器导航至
    http://127.0.0.1:8432/pr-review-{number}.html
    。
    为什么使用固定端口和
    cd /tmp
    :
    后台 shell 没有 TTY,因此 Python 会无限缓冲其启动消息(“Serving HTTP on...”);使用端口 0 意味着你永远无法读到所选端口。虽然
    --directory /tmp
    也可行,但
    cd /tmp
    在不同 Python 版本中更稳健。如果端口 8432 已被占用,请尝试 8433、8434 等。

Diff features (handled automatically by renderer.js)

Diff 功能(由 renderer.js 自动处理)

  • Filters out import-only lines
  • Collapses whitespace-only changes into context lines
  • Detects moved code blocks (3+ consecutive lines deleted in one place and added identically elsewhere) -- renders in blue/purple instead of red/green
  • Near-matches (moved + small edit) get a different purple tint
  • 过滤仅涉及 import 的行
  • 将纯空白改动折叠为上下文行
  • 检测移动的代码块(某处删除至少 3 行连续代码,且在别处以完全相同的内容新增)——以蓝色/紫色而非红/绿色呈现
  • 近似匹配(移动后带有小改动)使用不同的紫色色调

Style notes

样式说明

  • Dark theme:
    #1a1a1a
    background, Inter body font, IBM Plex Mono for code
  • Use
    var(--warning)
    for orange,
    var(--success)
    for green,
    var(--danger)
    for red,
    var(--accent)
    for blue
  • Sticky file headers (
    position: sticky; top: 0
    ) and notes (
    top: 35px
    ) pin while scrolling
  • Core files expanded by default (
    .file-body.open
    ), mechanical files collapsed
  • 深色主题:
    #1a1a1a
    背景,正文使用 Inter 字体,代码使用 IBM Plex Mono
  • 使用
    var(--warning)
    表示橙色、
    var(--success)
    表示绿色、
    var(--danger)
    表示红色、
    var(--accent)
    表示蓝色
  • 粘性文件标题(
    position: sticky; top: 0
    )和注释(
    top: 35px
    )会在滚动时固定在视口中
  • 核心文件默认展开(
    .file-body.open
    ),机械性文件默认折叠