Skip to content

feat(web): add file blame view to code browser#1160

Merged
brendan-kellam merged 5 commits intomainfrom
brendan/file-blame-ui
Apr 30, 2026
Merged

feat(web): add file blame view to code browser#1160
brendan-kellam merged 5 commits intomainfrom
brendan/file-blame-ui

Conversation

@brendan-kellam
Copy link
Copy Markdown
Contributor

@brendan-kellam brendan-kellam commented Apr 30, 2026

image image

Summary

Adds a GitHub-style file blame view to the code browser, accessible via a Code / Blame toggle next to the path header.

Visual features

  • Code / Blame segmented toggle next to the path header. URL-driven via the new ?blame=true query param so refresh, deep links, and back/forward all work. Hidden in preview-ref mode (where the existing "Previewing file at revision X" banner already covers that state).
  • File stats next to the toggle: 1,246 lines · 42.6 KB. Line count from newlines in the source; byte size from Buffer.byteLength on the already-fetched content (no extra git call).
  • Blame gutter rendered to the left of line numbers when blame is enabled. Each region's first line shows: relative date, author avatar (via the /api/avatar resolver — auto profile pictures where the email matches a Sourcebot user, identicon fallback), commit message (clickable → opens the focused commit diff for that commit), and a square-stack "reblame" button that walks one step back in history.
  • Cursor-driven peer highlighting (GitLens-style): when the cursor is on a line attributed to commit X, every other line attributed to X gets a subtle bg-accent highlight, in both the source and gutter columns.
  • Region grouping: porcelain emits a fresh group whenever source-line numbering is discontinuous in a commit's snapshot. The API now coalesces adjacent same-commit ranges so consumers see one range per visual region.

Architecture notes

  • /api/blame (already shipped in feat(web): add /api/blame endpoint #1158): server now coalesces adjacent same-commit ranges via a single pass in parsePorcelainBlame. Non-adjacent same-hash ranges are preserved as separate regions.
  • blameGutterExtension.ts: CodeMirror gutter built with raw DOM + Tailwind class strings (no React mounting in markers — earlier attempts hit flushSync lifecycle errors and async-render flicker). Uses Prec.high to register before lineNumbers() from basicSetup. A StateField derives the active commit from the cursor's main selection and provides Decoration.line decorations + a gutterLineClass RangeSet for the peer highlight.
  • pureCodePreviewPanel.tsx: when blame is on, disables foldGutter, highlightActiveLine, and highlightActiveLineGutter in basicSetup (they collide with the blame gutter and the cursor-driven peer highlight). Wires router-driven onCommitClick and onReblameClick callbacks to the extension.
  • UserAvatar: bypassed earlier-PR work — the avatar resolver and the raw <img> swap landed in feat(web): add /api/avatar resolver #1159, so the gutter inherits sync-paint avatars for free.

Drive-by

  • Includes one unrelated commit (fix: set activeBottomPanelTab to explore when goto refs / defs) that I picked up on the branch — feel free to ask me to split it out if you'd prefer it as its own PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Code/Blame toggle to switch views in the file preview.
    • Cursor-driven highlighting that marks all lines from the selected blame commit.
    • "Reblame" action to navigate to prior commits for a line range.
    • Blame UI shows commit metadata (author, message, relative date, optional avatar).
    • Visual age legend with color ramp for commit ages; header now shows line count and file size.
    • Improved handling when blame data fails to load.

brendan-kellam and others added 3 commits April 29, 2026 16:46
Wires up the /api/blame endpoint and CodeMirror gutter extension that
together render a GitHub-style blame view in the code preview panel.

URL state plumbing:
- New BLAME_QUERY_PARAM ('blame') in browse/hooks/utils.ts; getBrowsePath
  forwards `blame: true` as ?blame=true.
- page.tsx parses searchParams.blame and passes it to <CodePreviewPanel>.

Server-side:
- codePreviewPanel.tsx fetches blame data alongside file source via the
  existing parallel Promise.all when blame mode is enabled.
- getFileBlameApi.ts now coalesces adjacent same-commit ranges in porcelain
  output so the API surface presents one range per visual region (porcelain
  emits a fresh group whenever source-line numbering is discontinuous in
  the commit's snapshot, even when the final-file lines are contiguous and
  attributed to the same commit).

CodeMirror extension (blameGutterExtension.ts):
- Renders a 400px-wide gutter to the left of line numbers (Prec.high to
  jump it ahead of the basicSetup lineNumbers gutter).
- Each region's first line shows: relative date, author avatar (via the
  /api/avatar resolver), commit message, and a square-stack icon button
  for reblaming. Continuation lines are blank filler.
- Cells are built with raw DOM + Tailwind class strings (avoids React
  mounting in CM markers, which had async-render flicker and lifecycle
  errors when wrapped in flushSync).
- StateField + DecorationSet + gutterLineClass facet highlight every
  line of the cursor's commit (GitLens-style peer highlight) in both
  the source and gutter columns.
- Reblame button navigates to the previous commit's hash + path, with
  blame mode preserved, using full revisionName context shift.

pureCodePreviewPanel.tsx:
- Accepts blame data, mounts the extension when present, supplies the
  click and reblame callbacks (router-driven via getBrowsePath).
- Disables foldGutter, highlightActiveLine, and highlightActiveLineGutter
  in basicSetup when blame mode is on (they collide visually with the
  blame gutter and our cursor-driven peer highlight).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a segmented toggle next to the path header that switches between
"Code" (plain source) and "Blame" (gutter view). The toggle is hidden
when previewRef is set since the preview banner handles that state.

Also displays line count and file size next to the toggle (e.g.
"1,246 lines · 42.6 KB"). Line count is derived from the source string
(newlines, ignoring trailing); byte size uses Buffer.byteLength on the
already-fetched source (no extra git call).

Pulls in @radix-ui/react-toggle-group and a shadcn toggle-group.tsx
component to render the segmented control. Items are styled with
gap-0 + rounded-*-none + -ml-px to share a single border at the seam,
matching the GitHub-style segmented control look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds a git-blame mode for the code browser: a Code/Blame toggle, CodeMirror blame gutter showing per-line commit metadata and avatars, cursor-driven highlighting of lines sharing a commit, and a “reblame” action to navigate blame history.

Changes

Cohort / File(s) Summary
Changelog & package
CHANGELOG.md, packages/web/package.json
Added changelog bullet; bumped @radix-ui/react-toggle and added @radix-ui/react-toggle-group.
Blame gutter extension
packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/blameGutterExtension.ts
New CodeMirror Extension that builds per-line blame index from FileBlameResponse, renders a left blame gutter with commit info/avatars, reblame button, and decorations to highlight all lines for the active commit.
Blame UI & toggle
packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/blameViewToggle.tsx, packages/web/src/components/ui/toggle-group.tsx
New BlameViewToggle component and a Radix-backed ToggleGroup wrapper for segmented control styling/behavior.
Code preview integration
packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx, .../pureCodePreviewPanel.tsx
Added optional blame prop; conditional fetch of blame data; header UI updates (file stats, toggle, legend); wires blameGutterExtension into editor and navigation handlers for commit preview and reblame.
Page routing & URL utils
packages/web/src/app/(app)/browse/[...path]/page.tsx, packages/web/src/app/(app)/browse/hooks/utils.ts
Added blame=true search param support, BLAME_QUERY_PARAM constant, and blame?: boolean on blob BrowseProps; getBrowsePath appends blame query when set.
Blame parsing
packages/web/src/features/git/getFileBlameApi.ts
parsePorcelainBlame now merges contiguous ranges with the same commit hash to simplify ranges returned.
Blame age UI
packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/blameAgeColors.ts, .../blameAgeLegend.tsx
New 10-step age color ramp with bucket computation and a legend component rendering the color scale.
Navigation state
packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx
Now includes activeBottomPanelTab: 'explore' in navigation state when opening Explore for definitions/references.

Sequence Diagram

sequenceDiagram
    participant User
    participant Toggle as BlameViewToggle
    participant Router as Next.js Router
    participant Panel as CodePreviewPanel
    participant API as getFileBlame API
    participant Editor as PureCodePreviewPanel
    participant Gutter as blameGutterExtension

    User->>Toggle: select "Blame"
    Toggle->>Router: push URL with ?blame=true
    Router->>Panel: render with blame=true
    Panel->>API: fetch file + blame data
    API-->>Panel: return FileBlameResponse (ranges, commits)
    Panel->>Editor: pass blame data
    Editor->>Gutter: register extension with blame
    Gutter-->>Editor: render gutter (commits, avatars, reblame)
    User->>Editor: click commit message
    Editor->>Router: navigate to blob preview (diff=true)
    User->>Editor: click reblame
    Editor->>Router: navigate with previous hash & blame=true
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: adding a file blame view to the code browser, which aligns with the PR's primary objective of implementing GitHub-style blame functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/file-blame-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

License Audit

⚠️ Status: PASS

Metric Count
Total packages 2068
Resolved (non-standard) 12
Unresolved 0
Strong copyleft 0
Weak copyleft 39

Weak Copyleft Packages (informational)

Package Version License
@img/sharp-libvips-darwin-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.0.5 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-ppc64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-riscv64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-wasm32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-wasm32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-win32-arm64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
axe-core 4.10.3 MPL-2.0
dompurify 3.4.0 (MPL-2.0 OR Apache-2.0)
lightningcss 1.32.0 MPL-2.0
lightningcss-android-arm64 1.32.0 MPL-2.0
lightningcss-darwin-arm64 1.32.0 MPL-2.0
lightningcss-darwin-x64 1.32.0 MPL-2.0
lightningcss-freebsd-x64 1.32.0 MPL-2.0
lightningcss-linux-arm-gnueabihf 1.32.0 MPL-2.0
lightningcss-linux-arm64-gnu 1.32.0 MPL-2.0
lightningcss-linux-arm64-musl 1.32.0 MPL-2.0
lightningcss-linux-x64-gnu 1.32.0 MPL-2.0
lightningcss-linux-x64-musl 1.32.0 MPL-2.0
lightningcss-win32-arm64-msvc 1.32.0 MPL-2.0
lightningcss-win32-x64-msvc 1.32.0 MPL-2.0
Resolved Packages (12)
Package Version Original Resolved Source
@react-grab/cli 0.1.23 UNKNOWN MIT GitHub repo aidenybai/react-grab (same maintainer aiden.bai05@gmail.com, MIT license confirmed)
@react-grab/cli 0.1.29 UNKNOWN MIT GitHub repo aidenybai/react-grab (same maintainer aiden.bai05@gmail.com, MIT license confirmed)
@react-grab/mcp 0.1.29 UNKNOWN MIT GitHub repo aidenybai/react-grab (same maintainer aiden.bai05@gmail.com, MIT license confirmed)
codemirror-lang-elixir 4.0.0 UNKNOWN Apache-2.0 GitHub repo livebook-dev/codemirror-lang-elixir (GitHub license API)
element-source 0.0.3 UNKNOWN MIT GitHub repo aidenybai/element-source (same maintainer aiden.bai05@gmail.com, MIT license confirmed)
lezer-elixir 1.1.2 UNKNOWN Apache-2.0 GitHub repo livebook-dev/lezer-elixir (GitHub license API)
map-stream 0.1.0 UNKNOWN MIT GitHub repo dominictarr/map-stream (GitHub license API)
memorystream 0.3.1 UNKNOWN MIT npm registry licenses array: [{"type":"MIT","url":"http://github.com/JSBizon/node-memorystream/raw/master/LICENSE"}]
obug 2.1.1 UNKNOWN MIT npm registry license field
pause-stream 0.0.11 ["MIT","Apache2"] MIT AND Apache-2.0 GitHub repo dominictarr/pause-stream LICENSE file (dual-licensed MIT and Apache 2)
posthog-js 1.369.0 SEE LICENSE IN LICENSE Apache-2.0 GitHub repo PostHog/posthog-js LICENSE file (Apache License, Version 2.0)
valid-url 1.0.9 UNKNOWN MIT GitHub repo ogt/valid-url LICENSE file

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/app/(app)/browse/[...path]/page.tsx (1)

111-137: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Suppress blame mode when a preview ref is active.

searchParams.blame still reaches CodePreviewPanel when previewRef is set, so preview mode can fetch/render blame data even though the toggle is hidden. Gate it here so preview-ref URLs stay source/diff-only.

🔧 Proposed fix
-    const isBlameMode = searchParams.blame === 'true';
+    const isBlameMode = searchParams.blame === 'true' && !previewRef;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/`(app)/browse/[...path]/page.tsx around lines 111 - 137,
The blame flag from searchParams still gets passed to CodePreviewPanel even when
a preview ref is active; compute an effective blame value (e.g., const
effectiveBlame = isBlameMode && !previewRef) and pass that to CodePreviewPanel
instead of isBlameMode so that when previewRef is set (preview mode) blame is
suppressed; update the CodePreviewPanel prop (blame={...}) accordingly and keep
previewRef, isDiffMode, and focused diff logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/web/src/app/`(app)/browse/[...path]/components/codePreviewPanel/blameGutterExtension.ts:
- Around line 72-80: The reblame button created in blameGutterExtension.ts uses
only title for identification which is not sufficient for assistive tech; update
the reblameBtn element (const reblameBtn) to include an explicit accessible name
by setting an appropriate aria-label (e.g., `aria-label="Blame prior to
<short-hash>"`) that mirrors the title and includes the previous.hash.slice(0,7)
so screen-reader and keyboard users can discover the action; keep the existing
title and event handler (onReblameClick) unchanged.

---

Outside diff comments:
In `@packages/web/src/app/`(app)/browse/[...path]/page.tsx:
- Around line 111-137: The blame flag from searchParams still gets passed to
CodePreviewPanel even when a preview ref is active; compute an effective blame
value (e.g., const effectiveBlame = isBlameMode && !previewRef) and pass that to
CodePreviewPanel instead of isBlameMode so that when previewRef is set (preview
mode) blame is suppressed; update the CodePreviewPanel prop (blame={...})
accordingly and keep previewRef, isDiffMode, and focused diff logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44904e11-aaed-47ba-950c-a8e9be0b1e16

📥 Commits

Reviewing files that changed from the base of the PR and between 6faeb6d and 8e719ed.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (11)
  • CHANGELOG.md
  • packages/web/package.json
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/blameGutterExtension.ts
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/blameViewToggle.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/pureCodePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/page.tsx
  • packages/web/src/app/(app)/browse/hooks/utils.ts
  • packages/web/src/components/ui/toggle-group.tsx
  • packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx
  • packages/web/src/features/git/getFileBlameApi.ts

@brendan-kellam brendan-kellam merged commit 38d2274 into main Apr 30, 2026
8 of 9 checks passed
@brendan-kellam brendan-kellam deleted the brendan/file-blame-ui branch April 30, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant