feat(node-ui): add shared context graph empty states #2574
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Codex PR Review | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| paths-ignore: | |
| - 'pnpm-lock.yaml' | |
| - 'LICENSE' | |
| - 'knip.json' | |
| - 'CHANGELOG.md' | |
| - 'data/**' | |
| - 'snapshots/**' | |
| - '**/*.snap' | |
| - '**/*.log' | |
| - '**/CHANGELOG.md' | |
| - '**/dist/**' | |
| - '**/.turbo/**' | |
| - '**/node_modules/**' | |
| - '**/snapshots/**' | |
| concurrency: | |
| group: codex-review-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| # Default everything to read-only; the one job that posts review comments | |
| # escalates `pull-requests: write` at the job level only. | |
| permissions: | |
| contents: read | |
| jobs: | |
| review: | |
| name: Codex Review | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| # 30 min ceiling: gpt-5.4 / effort: medium can take ~25 min on a | |
| # large diff, so 15 min was hitting the cap mid-stream. The | |
| # concurrency group above already cancel-in-progress, so a new push | |
| # will kill any still-running review — no downside to a generous | |
| # ceiling. | |
| timeout-minutes: 30 | |
| # Skip fork PRs (no access to secrets) and draft PRs (still iterating) | |
| if: | | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| && github.event.pull_request.draft == false | |
| steps: | |
| - name: Checkout PR merge commit | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: refs/pull/${{ github.event.pull_request.number }}/merge | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Generate filtered PR diff | |
| id: diff | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: | | |
| # Filter out noise paths from the diff sent to the model. | |
| git diff "$BASE_SHA"...HEAD -- \ | |
| ':!pnpm-lock.yaml' \ | |
| ':!LICENSE' \ | |
| ':!knip.json' \ | |
| ':!CHANGELOG.md' \ | |
| ':!data/**' \ | |
| ':!snapshots/**' \ | |
| ':!**/*.snap' \ | |
| ':!**/*.log' \ | |
| ':!**/CHANGELOG.md' \ | |
| ':!**/dist/**' \ | |
| ':!**/.turbo/**' \ | |
| ':!**/node_modules/**' \ | |
| ':!**/snapshots/**' \ | |
| > pr-diff.patch | |
| DIFF_LINES=$(wc -l < pr-diff.patch) | |
| DIFF_HASH=$(sha256sum pr-diff.patch | cut -d' ' -f1) | |
| echo "diff_lines=${DIFF_LINES}" >> "$GITHUB_OUTPUT" | |
| echo "diff_hash=${DIFF_HASH}" >> "$GITHUB_OUTPUT" | |
| echo "Filtered diff: ${DIFF_LINES} lines, hash ${DIFF_HASH}" | |
| - name: Check if diff already handled (hash skip) | |
| id: hash_check | |
| if: steps.diff.outputs.diff_lines != '0' | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | |
| env: | |
| DIFF_HASH: ${{ steps.diff.outputs.diff_hash }} | |
| with: | |
| script: | | |
| const marker = `<!-- codex-review-diff-hash: ${process.env.DIFF_HASH} -->`; | |
| const reviews = await github.paginate( | |
| github.rest.pulls.listReviews, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| per_page: 100, | |
| } | |
| ); | |
| const matched = reviews.some(r => r.body && r.body.includes(marker)); | |
| if (matched) { | |
| console.log('Diff hash matches a prior review or skip notice — silently skipping.'); | |
| core.setOutput('skipped', 'true'); | |
| } else { | |
| core.setOutput('skipped', 'false'); | |
| } | |
| - name: Skip if diff exceeds size cap | |
| id: size_check | |
| if: | | |
| steps.diff.outputs.diff_lines != '0' | |
| && steps.diff.outputs.diff_lines > 5000 | |
| && steps.hash_check.outputs.skipped == 'false' | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | |
| env: | |
| DIFF_HASH: ${{ steps.diff.outputs.diff_hash }} | |
| DIFF_LINES: ${{ steps.diff.outputs.diff_lines }} | |
| with: | |
| script: | | |
| const hashMarker = `<!-- codex-review-diff-hash: ${process.env.DIFF_HASH} -->`; | |
| const lines = process.env.DIFF_LINES; | |
| await github.rest.pulls.createReview({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| body: `${hashMarker}\nCodex review skipped: filtered diff is ${lines} lines (cap: 5,000). Please consider splitting this into smaller PRs for reviewability.`, | |
| event: 'COMMENT', | |
| comments: [], | |
| }); | |
| - name: Run Codex review | |
| id: codex | |
| if: | | |
| steps.diff.outputs.diff_lines != '0' | |
| && steps.diff.outputs.diff_lines <= 5000 | |
| && steps.hash_check.outputs.skipped == 'false' | |
| uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1 # v1.8 | |
| with: | |
| openai-api-key: ${{ secrets.OPENAI_API_KEY }} | |
| model: gpt-5.4 | |
| prompt-file: .codex/review-prompt.md | |
| output-schema-file: .codex/review-schema.json | |
| effort: medium | |
| sandbox: read-only | |
| - name: Post PR review with inline comments | |
| if: | | |
| steps.diff.outputs.diff_lines != '0' | |
| && steps.diff.outputs.diff_lines <= 5000 | |
| && steps.hash_check.outputs.skipped == 'false' | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | |
| env: | |
| REVIEW_JSON: ${{ steps.codex.outputs.final-message }} | |
| DIFF_HASH: ${{ steps.diff.outputs.diff_hash }} | |
| with: | |
| script: | | |
| const raw = process.env.REVIEW_JSON || ''; | |
| const hashMarker = `<!-- codex-review-diff-hash: ${process.env.DIFF_HASH} -->`; | |
| console.log(`Raw Codex output (${raw.length} chars): ${raw.slice(0, 1000)}`); | |
| let review; | |
| try { | |
| review = JSON.parse(raw); | |
| } catch (e) { | |
| console.error('Failed to parse Codex output:', e.message); | |
| // Transient failure: do NOT embed the hash marker, so the next | |
| // run on this same diff retries instead of silently skipping. | |
| await github.rest.pulls.createReview({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| body: `Codex review failed to produce valid JSON output. Check the [workflow logs](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}) for details.`, | |
| event: 'COMMENT', | |
| comments: [], | |
| }); | |
| return; | |
| } | |
| // Fetch all changed files (paginated for large PRs) | |
| const files = await github.paginate( | |
| github.rest.pulls.listFiles, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| per_page: 100, | |
| } | |
| ); | |
| // Build set of valid (path:line) pairs from right-side diff hunk lines | |
| // (added + context). This keeps comments bound to changed areas. | |
| const validLines = new Set(); | |
| for (const file of files) { | |
| // Skip binary/large/truncated files with no patch | |
| if (!file.patch) continue; | |
| const lines = file.patch.split('\n'); | |
| let currentLine = 0; | |
| for (const line of lines) { | |
| const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/); | |
| if (hunkMatch) { | |
| currentLine = parseInt(hunkMatch[1], 10); | |
| continue; | |
| } | |
| // Added lines are valid comment targets | |
| if (line.startsWith('+')) { | |
| validLines.add(`${file.filename}:${currentLine}`); | |
| currentLine++; | |
| continue; | |
| } | |
| // Deleted lines do not exist in the new file | |
| if (line.startsWith('-')) continue; | |
| // Ignore hunk metadata lines | |
| if (line.startsWith('\\')) continue; | |
| // Context lines on the right side are also valid targets | |
| validLines.add(`${file.filename}:${currentLine}`); | |
| currentLine++; | |
| } | |
| } | |
| console.log(`Valid comment locations: ${validLines.size}`); | |
| // Partition comments into valid (on right-side diff lines) and dropped | |
| const comments = Array.isArray(review.comments) ? review.comments : []; | |
| const validComments = []; | |
| const droppedComments = []; | |
| for (const comment of comments) { | |
| const key = `${comment.path}:${comment.line}`; | |
| if (validLines.has(key)) { | |
| validComments.push({ | |
| path: comment.path, | |
| line: comment.line, | |
| body: comment.body, | |
| side: 'RIGHT', | |
| }); | |
| } else { | |
| droppedComments.push(comment); | |
| } | |
| } | |
| if (droppedComments.length > 0) { | |
| console.log('Dropped out-of-diff comments:'); | |
| for (const c of droppedComments) { | |
| console.log(` ${c.path}:${c.line} — ${c.body.slice(0, 80)}`); | |
| } | |
| } | |
| if (validComments.length === 0) { | |
| console.log(`No valid inline comments (${comments.length} total from Codex, ${droppedComments.length} dropped).`); | |
| // "All dropped" means the model produced comments but every one | |
| // targeted lines outside the right-side diff — usually a line | |
| // mapping failure, not a real no-issues signal. Treat it as | |
| // transient: post the warning without the hash marker so the | |
| // next run on the same diff retries instead of silently skipping. | |
| const isLineMappingFailure = droppedComments.length > 0; | |
| const summary = isLineMappingFailure | |
| ? `Codex review produced ${droppedComments.length} comment(s) but all targeted lines outside the diff and were dropped. Check the [workflow logs](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}) for details.` | |
| : 'Codex review completed — no issues found.'; | |
| const body = isLineMappingFailure ? summary : `${hashMarker}\n${summary}`; | |
| await github.rest.pulls.createReview({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| body, | |
| event: 'COMMENT', | |
| comments: [], | |
| }); | |
| return; | |
| } | |
| // Inline comments + a small body carrying the diff-hash marker so | |
| // future runs can detect "diff unchanged" and skip. | |
| await github.rest.pulls.createReview({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| body: hashMarker, | |
| event: 'COMMENT', | |
| comments: validComments, | |
| }); | |
| console.log( | |
| `Review posted: ${validComments.length} inline comments, ${droppedComments.length} dropped out-of-diff comments` | |
| ); |