Skip to content

Add blame information to the diff pane (sticky strip + age heatmap) #12

@geevensingh

Description

@geevensingh

Problem

When reviewing a diff, the reviewer often needs to know who last
touched
the surrounding code and how old it is — both for routing
("who do I ask about this") and for risk assessment ("modifying
month-old churn is different from modifying year-stable code"). Git
provides git blame for this, but DiffViewer surfaces none of it
today.

Proposal

Add a two-layer blame surface to the diff pane:

Layer 1 — Gutter age heatmap (always-on when blame is enabled)

A thin (~4 px) colored band between the line number and the code,
colored by commit age:

  • Recent (this week) — warm orange-red
  • 1–3 months — amber / gold
  • 3–6 months — olive
  • 6 months – 1 year — neutral warm
  • 1+ year — neutral gray

Local edits on the working-tree side render as a dashed teal band so
uncommitted lines are visually distinct from committed ones.

A small legend is reachable via a "?" affordance on the heatmap band
or via Settings.

Layer 2 — Sticky current-line blame strip (focus-driven)

A single-row strip below each pane reveals blame for the focused line:

▸  e4f5a6b   R. Sundar   4 months ago   "Render hunks asynchronously"

The strip is visible only when blame is enabled. Right-click on the
strip exposes the action surface:

  • Compare this commit to its parent — launches a new DiffViewer
    context (reuses the launch-context plumbing).
  • Copy SHA
  • Blame this file standalone — opens a file-blame view (out of
    scope for this issue, but the menu entry reserves the slot).

Layer 3 — Per-line annotation (opt-in expert mode)

A separate, toggle-able "show full blame per line" mode that renders
SHA / author / relative date in a wider gutter for every line. This is
the classic Fork / git blame layout. It costs ~170 px per pane and
crowds side-by-side mode, so it's opt-in only, not the default.

Scope decisions

  • Two-layer default (heatmap + strip), opt-in third layer
    (per-line).
    A mockup comparison ruled out always-on per-line for
    side-by-side mode.
  • Working-tree side blame is computed against HEAD, with
    uncommitted lines marked as "local edit" (dashed band + "uncommitted /
    now" in the strip). Treating the working tree as if it were HEAD is
    the simplest honest semantics.
  • Background-thread computation. LibGit2Sharp.Repository.Blame
    can take hundreds of ms to multiple seconds on large files. Blame
    must run off the UI thread (per AGENTS.md §9) and feed results into
    the render pipeline incrementally — never block the diff itself.
  • Per-(repo path, commit SHA, file path) cache. Blame is
    expensive and only changes when commits are added. Cache lifetime
    follows the existing repository-watcher invalidation surface.
  • Settings. One toggle to enable the default surface (heatmap +
    strip together). A second toggle for the opt-in per-line mode.
    Reuses the existing settings persistence path.

Open questions

  • Granularity of the default-surface toggle — single switch for
    both layers, or separate heatmap-on / strip-on toggles. Single is
    simpler; separate gives users who only want one or the other a
    cleaner choice.
  • Default state at first launch — both off (opt-in everything),
    heatmap on / strip on but small, or heatmap off / strip on (least
    noisy default that still adds value). Probably "both off" since
    blame computation has a measurable cost on first open and we
    shouldn't pay it for users who don't want it.
  • Rename / copy detection knob. git blame -C -M follows renames
    and copies aggressively. LibGit2Sharp's default does not. Decide
    the default explicitly — "follow renames" matches user intuition;
    "don't follow" is faster.
  • Heatmap color scale calibration. The age bands ("recent",
    "1–3 months", etc.) are intuitive but somewhat arbitrary. May want
    the scale to adapt to the repo's median commit age so a 5-year-old
    repo doesn't render entirely in the "neutral gray" bucket.
  • Click-through navigation from a blamed line. The right-click
    "Compare commit to its parent" entry needs to play nicely with the
    recent-contexts store — adding the launched context to recents on
    every blame click might pollute the list.
  • Inline mode behavior. The same two-layer surface should work in
    inline mode, but only one heatmap band (the inline view has a
    single editor), and the strip lives at the bottom of the single
    pane.

Acceptance

  • Settings toggles for the default surface (heatmap + strip) and the
    per-line annotation mode.
  • Heatmap band renders next to every line in both side-by-side and
    inline modes when enabled.
  • Sticky strip renders below the pane for the focused line, with
    right-click → "Compare commit to its parent" working end-to-end.
  • Blame computation runs on a background thread; the diff itself
    never blocks waiting on blame.
  • Per-(repo, SHA, path) blame cache invalidates when the
    repository-watcher fires.
  • Working-tree side renders uncommitted lines as "local edit" (dashed
    band + uncommitted label).
  • Per-line annotation mode renders correctly when toggled on.
  • Tests cover: the blame-service layer (new), the cache invalidation
    hook, the heatmap age-bucket assignment, and the
    view-model surface that drives the strip.

Notes

Tier 3 from the feature brainstorm, but elevated by a deliberate
design pass — the two-layer composition is what makes blame fit
DiffViewer's "quiet by default" chrome philosophy without sacrificing
information density when the user asks for it. Scope: medium.

A mockup comparing baseline / A (strip alone) / B (heatmap alone) /
C (per-line) / A+B (recommended) was used to settle the composition.
The trade-off table:

Option Visual noise Horizontal cost Info per line Discoverability
Baseline none 0 px none n/a
A — Sticky strip low 0 px full (focused line) needs click / hover
B — Heatmap low 4 px age only needs legend
C — Per-line high ~170 px full (every line) always visible
★ A + B (this issue) low 4 px age always + full focus heatmap + focus strip

Related: the existing IExternalAppLauncher / right-click action
plumbing is the model for the strip's context menu. The recent
file-list refresh fix (v0.3) is the model for keeping blame state
intact across same-file refreshes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-designSubstantive open questions to resolve before implementationpriority: lowNice to have / deferredscope: mediumMulti-day, spans a couple subsystems

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions