Skip to content

hue diff & PR view — Feature Requirements

Status: planned · Date: 2026-08-04 (waves 2–3 scoped 2026-08-05, the type overlay 2026-08-06) · Scope: viewing diffs (two files, a piped unified patch, git revisions), then pull requests (second wave), then a write surface (third wave): hunk/line staging, inline editing, content-anchored comments with proposed suggestions, and 3-way conflict viewing/resolution — across all four sinks (GUI / TUI / ANSI / HTML), over a new sparkles:diff engine library; plus resolved D types on both sides of the diff (DVT) over sparkles:twoslash-d. The motivating pain: editing a large markdown table and having the formatter re-align unrelated rows, drowning the real change in alignment noise — so noise classification is a first-class concern, not an afterthought.

NOTE

Forward-looking — every row is not started. Status legend and IDs: see the overview. The prior-art survey feeding this spec is docs/research/diff-review/.

Design & rationale

Fifteen decisions shape the spec (1–7 settled 2026-08-04, 8–12 on 2026-08-05, 13–15 on 2026-08-06):

  1. Diffs first, PRs next. The first wave renders local diffs; PR viewing (DPR) is specced now but built as a second wave on the same diff session — a PR is "a multi-file diff plus markdown metadata plus anchored comment threads", all three of which hue already knows how to render or will after the first wave.
  2. Diff is a content kind, not a mode. Per the dispatch collapse (MOD note), a diff session is a new ContentKind-like document value produced once and rendered by every sink — no per-sink diff pipelines. A multi-file diff is a SourceSet-style session (SRC6) the explorer navigates.
  3. The engine is a library. Diff computation (line diff, patch parsing, word-level refinement, structural classification) lands in a new sparkles:diff library, not in apps/huesparkles:test-utils today shells out to delta for test diffs (diff_tools.d) and can migrate, and future tooling (release, ci) wants the same primitives.
  4. Noise is layered, all four layers. Word-level refinement (baseline) → formatting-only hunk classification (cheap, text-level) → structural tree-sitter diff (precise, grammar-gated) → rendered-preview diff (the novel markdown move). Each layer degrades to the one before it. The last one is not just a stronger filter but a different unit: layers 1–3 all diff lines, and for prose the line is the wrong unit — rewrapping a paragraph changes every line of it without changing a word. Layer 4 diffs the document model, where padding and wrapping are not content at all.
  5. sparkles:diff is tree-sitter-free. The engine core (line diff, pairing, patch parsing, word refinement) has no sparkles:tree-sitter dependency; the structural pass (DVN3) lives where sparkles:syntax is already present. sparkles:test-utils can then depend on the core cheaply. And it is @nogc end to end (DVM8): SmallBuffer flat arenas, borrowed span-based texts, output-range emitters.
  6. Structural diffing auto-engages when cheap (DVN3): whenever a grammar exists and the file is under the ceilings, with --diff-structural=on|off as the override — best-default UX over conservatism. The parser may only ever demote a change, never promote or hide one: unknown (no grammar, a guard tripped, a parse that did not succeed) is no claim, and every caller treats it exactly like "differs". The view it can also drive (grammar-token emphasis) is opt-in on top, because that is a different reading of the same diff rather than a strictly better one — prose is served better by word runs.
  7. PR fetch is native. hue talks to the GitHub REST/GraphQL API directly from D (no gh binary dependency); tokens are discovered, never prompted for (DPR1).
  8. The viewer has a write surface — after PR reading. hue's diff session supports staging (hunk/line, DST2), inline editing of the worktree side (DST5), and commenting with proposed suggestions (DCM) — sequenced as the third wave, after PR viewing, so review reading ships first and the comment machinery is then reused for review submission (DCM6).
  9. Conflicts are in scope, as their own area (CFV): 3-way conflict viewing and resolution, entered by parsing conflict-markered files back into a base/ours/theirs model (Mergiraf ParsedMerge precedent). The DVM2 pairing pass is N-way capable (Neovim linematch precedent) so the 3-way panes reuse it.
  10. Review state lives in a versioned git ref (refs/hue/data, the git-spice pattern): draft comments, viewed marks, and fold state as JSON blobs with commit history — worktree-shared, no working-tree pollution, git log --patch as the free debug UI (DCM5).
  11. Suggestions serialize twice (DCM4): a suggestion is a model-level range replacement emitted both as a GitHub suggestion fence (PR reviews) and as a git-applyable patch (local review). One model, two emitters.
  12. One forge seam, many forges (DPR7): everything above the fetch layer talks to a forge interface with Design-by-Introspection optional-capability discovery (the git-spice WithComparisonURL precedent: hasMember-style probing, never if (forge == …) branching), so GitHub is merely the first adapter and GitLab / Gitea / Forgejo / Codeberg follow without touching the session or UI layers; a missing capability degrades that one feature, not the forge.
  13. The old side is analyzed in a real worktree, not approximated. Types on both sides (DVT) are only worth having if they are right: a confidently wrong type in a review tool is worse than no type. DMD-as-a-library resolves imports by reading other modules from disk, so the old side is analyzed inside a materialized git worktree of that revision (detached, machine- managed, under hue's cache root) — siblings, dub.sdl and selections all at the revision under review. The worktree slice this needs (add --detach / remove / prune / list --porcelain) goes into the shared sparkles:git library rather than being hand-rolled in apps/hue: that extraction is already decided (dman D16 — the release/git.d argv funnel promoted to libs/git with the spawner injected as a capability, worktree verbs explicitly listed as net-new there), and hue's type overlay is simply its first consumer, with dman's VcsRepo the second. Layout follows dman's convention (D9), with hue using its own cache root — these are machine-managed caches, not a user's branch worktrees.
  14. One analysis serves its whole import closure. Analyzing a.d already made DMD analyze everything a imports, so opening b.d next must not start a second process. The oracle therefore enumerates the set of modules its one analysis covers and answers payload/tip requests for any file in that set (EXT8), and hue caches payloads per (revision, path). Navigation inside a closure is instant; a new process is spawned only for a file the current analyses do not cover. This is what makes the per-side process budget bearable at PR scale.
  15. The semantic differential is staged, coarse before precise. Session-wide the analyzer yields a cheap per-file verdict badge (public signatures changed / implementation only / doc only / type-preserving, DVT4) from an API-surface digest of each side; the precise per-identifier comparison — which inferred type changed, which call now resolves to a different overload (DVT5) — is computed only when the file is actually opened. The reviewer gets the map immediately and the detail where they look.

Diff engine & document model (DVM)

The proposed sparkles:diff library (libs/diff).

IDRequirementStatusTraces to
DVM1A line-level diff of two in-memory texts must produce a backend-neutral diff document: files → hunks → rows, each row context / added / removed / changed-pair, with old/new line numbers. Algorithm: Myers as the baseline; histogram/patience variants are an internal choice, not API surface.full (440ab1f7)libs/diff myers.d/model.d
DVM2Within a change block the model must carry alignment pairs — which removed line corresponds to which added line, chosen by similarity, not naive order — the substrate for side-by-side rows (DVL2) and word-level refinement (DVM4). The pairing pass must be N-way capable (2+ documents — the Neovim linematch precedent, a post-pass DP that ships independently of the base algorithm) so the 3-way conflict panes (CFV2) reuse it.partial (440ab1f7)libs/diff pairing.d (2-way; N-way for CFV2 pending)
DVM3A unified-patch parser must ingest git diff / diff -u output — file headers, hunk headers, \ No newline at end of file, rename/copy/mode lines, binary markers — into the same model, so a parsed and a computed diff render identically.partial (440ab1f7)libs/diff patch.d; a bare --- closes the previous file so plain diff -u/-ru output splits (b3adf6a1) — mode/index metadata not yet retained
DVM4Word-level refinement: paired changed lines must get a sub-line diff marking changed segments; word-boundary tokenization as the baseline, with a hook for smarter tokenizers.partial (440ab1f7)libs/diff refine.d (baseline tokenizer + guards; smarter-tokenizer hook pending)
DVM5Diff rendering must compose with highlighting, not replace it: both sides of each file flow through the existing engine (ENG1ENG4) and the diff decorations layer on top (delta-style).full (7f748568)diff_view.composedSpans + per-file DiffSides in every sink; pair and patch paths both compose
DVM6Scale guards: per-file and per-diff ceilings must degrade expensive passes (refinement, structural) to the plain line diff — never a hang or a crash. Binary files render as a one-line notice.partial (440ab1f7)libs/diff DiffOptions caps + Degradation (the binary one-line notice is sink-side, V1)
DVM7The library must be testable without git or a terminal: pure functions over strings, golden tests for the model, and property tests (diff+patch round-trips).full (440ab1f7)libs/diff unittests (goldens incl. the re-padded-table scenario; LCS-reconstruction + round-trip properties)
DVM8The engine is @nogc: @safe pure nothrow @nogc across every path — diff, pairing, refinement, patch parse/emit. Owning storage is sparkles.base.smallbuffer.SmallBuffer (the vector-with-SBO, copy-on-write container); the model is a flat arena of plain-data elements (indices + spans — SmallBuffer elements must be pointer-free in @safe code) with texts borrowed as spans resolved through DiffDoc accessors; emitters write to caller-supplied output ranges. Consumers may convert at their own boundary (hue is GC-land); the library itself never GC-allocates.full (440ab1f7)libs/diff (attribute-annotated surfaces); sparkles.base.smallbuffer

Diff sources (DVS)

IDRequirementStatusTraces to
DVS1hue --diff <old> <new> with two file arguments must compute the diff in-process (DVM1) — no VCS involved.full (31fdab59)document.loadDiffPair; --diff in app.main
DVS2A piped unified diff must render as a diff: git diff | hue — detected by non-tty stdin + content sniff (diff --git / --- / +++ / @@), or forced with --patch. When the new-side files are readable from the worktree, re-highlight from full sources; else highlight the patch text alone. This makes hue usable as a core.pager-adjacent viewer, like delta.full (7f748568)document.fromPatchSource/looksLikePatch + sidesFromWorktree/reconstructOldText (validated reverse-apply; stale worktrees degrade to plain rows)
DVS3Git revisions: hue --diff [<rev>[..<rev>]] [-- <path>…] must shell out to git diff (porcelain, pinned flags, --no-color) — worktree vs HEAD by default, --staged for the index; old-side contents via git show <rev>:<path>. No libgit2 dependency in the first wave; repo-root detection reuses git_status.d.full (a2b634e8)document.loadGitDiff/gitDiffSideSpecs/sidesFromGit — each side fetched exactly, so revision diffs compose syntax instead of reverse-applying
DVS4A multi-file diff must build one diff session (an ordered changed-file list with per-file status), the same session substrate every sink consumes (SRC6 analog) and the explorer pane navigates with GitStatus glyphs.full (2ac6dd0b)diff_session.d (DiffSession: ordered entries, status, counts, fold state) built by every diff loader, rendered by all four sinks, and listed by the explorer as a changed-files tree (TVU6)
DVS5Degradation: an empty diff renders "no changes" (not a crash or blank screen); unreadable sides report per-file errors and keep the rest of the session.full (ddf2decb)viewDiffDoc in-band notices + per-entry SessionEntry.error (both sides unfetchable ⇒ that file alone reports, the session renders)

Layout & rendering (DVL)

IDRequirementStatusTraces to
DVL1Unified layout: one column; a dual line-number gutter (old · new); +/-/context markers; add/remove/changed row backgrounds layered over syntax highlighting.full (79206f89)diff_view.viewDiffDoc — unified layout with dual gutter, markers, tints, and DVM5 syntax composition
DVL2Side-by-side layout: two panes with aligned rows from DVM2, filler rows opposite unmatched lines, and wrapping that keeps the panes in lockstep (a wrapped row advances both panes).full (a25a80f7)diff_view.alignSplitRows (pure, index-only) + viewHunkSplit; fillers opposite unmatched lines, percent(50) halves so the divider cannot wander with the text
DVL3Both layouts ship in the first wave behind --diff-layout unified| full (a25a80f7`)--diff-layout=unified|split, s toggles in both panes (ViewerModel.diffToggleLayout), and a pane under minSplitWidth (80) degrades to unifiedproposed CLI + keymap
DVL4All four sinks render the diff session from the one model: GUI and TUI via one shared sparkles:ui widget view, non-interactive ANSI whole-emit, and HTML — gallery-integrable, with selection domains so copying one side of a split never grabs the other (HTM8 analog).full (8b4f9797)all four sinks paint the one model: ANSI/HTML directly, TUI/GUI via ViewerModel's diff branch (viewDiffDoc); Tab toggles the raw patch view
DVL5Diff colors are theme slots (added/removed/changed line backgrounds, emphasized word segments, hunk header, filler) resolved per theme like every other slot — no hardcoded RGB.full (73de65bb)sparkles.ui.style diff slots + defaultTwoslashPalette seeding
DVL6Intra-line changed segments (DVM4) render with a second emphasis level above the line background (delta's two-tone emphasis).full (6569e05b)diff_view.contentSpans two-tier emphasis
DVL7A markdown file in a diff renders source by default (diff of the text); the decorated-preview diff is its own mode (DVN6), not the default.not startedMOD8 interaction
DVL8Copy modes: copying a diff selection honors --diff-copy=text|patch (default text) — plain text of the selected side, or a valid unified sub-patch of the selection (the --table-copy precedent, CLI11); runtime-toggleable, honored by GUI clipboard, TUI OSC 52, and HTML selection alike.not startedproposed copy serializers; CLI13

Backend availability

The write surface is not four-sink. Viewing is; writing needs an interactive backend, and Android phases in behind the editor component:

CapabilityTUIGUI desktopGUI AndroidHTML
Diff viewing (layouts, noise, preview-diff)yesyesyesyes (static)
Type overlay + verdict badges (DVT)yesyesno — no analyzer on deviceyes (CSS popups)
Staging / discard (DST2DST4)yesyesyes (touch, AND12)no — read-only
Inline editing (DST5)yesyeslater (soft-keyboard editor, UIA9)no
Comments: reading (DPR3/DCM2)yesyesyesyes (static)
Comments: authoring / suggestions (DCM1/DCM3)yesyesyes (composer, phased)no
Conflict resolution (CFV4)yesyeslaterno

Noise handling (DVN)

The four-layer strategy; each layer independent and degradable.

IDRequirementStatusTraces to
DVN1Whitespace toggles: ignore leading/trailing/all whitespace and blank-line-only changes — CLI flags plus runtime toggles, applied at the engine level (the ignored difference never reaches the model as a change).full (095bd5d6)sparkles.diff.normalize (WhitespaceMode: git's exact/trailing/change/all vocabulary) applied at the line-interning seam, so an ignored difference is never a change in the model; --diff-ignore-whitespace exposes it
DVN2Formatting-only hunk classification: a hunk whose sides are equivalent after whitespace collapsing (and, where a grammar exists, after token-stream comparison) is tagged formatting-only; it renders dimmed and folded by default with a count badge ("3 formatting-only hunks"), expandable per-hunk or globally.full (1b96f740)sparkles.diff.classify stamps Hunk.formattingOnly (conservative: one real edit makes the hunk real; re-aligned tables incl. their separator count as noise via DVN4), diff_view folds it to a dimmed row-count badge, zn expands
DVN3Structural diff mode: parse both sides with tree-sitter (sparkles:syntax's engine), diff at the node level, and classify token-stream-identical changes as unchanged — difftastic's territory. Auto-engages when a grammar exists and the file is under the ceilings (DVM6); --diff-structural=on| full (17d0ad14`)apps/hue/src/diff_structural.d compares the two sides' TOKEN STREAMS under the grammar — reaching reflowed code that DVN1/DVN2 structurally cannot. Verdicts at two granularities from one pair of parses: whole file, and per hunk over the tokens OVERLAPPING its line range (overlap, not start-inside, so a block comment opening above the hunk still counts). --diff-structural=auto|on|off|viewon waives the size ceiling, view picks the structural view at load and S swaps it at runtime. The view (diff_token_view) takes intra-line emphasis boundaries from the grammar instead of word/space classes: alpha+betaalpha - beta emphasizes +/-, not -. It reuses the engine's LCS via refinePairTokens, so sparkles:diff stays tree-sitter-free (decision 5). NOT done: node-level (tree-edit-distance) alignment across lines — the view is token-granular within paired rowssparkles:syntax ts engine; proposed mode
DVN4Markdown-table cell diffing — the motivating scenario, golden-tested: a changed pipe-table row highlights only the changed cells (cell boundaries from the markdown grammar / MdDoc), and rows differing only in cell padding classify as formatting-only (DVN2). The golden test is: reformat a large table + edit one cell → exactly one cell lights up.full (1b96f740)sparkles.diff.table (cell spans + rowsEquivalent, tree-sitter-free: a pipe row is a lexical fact) feeding cell-wise refinement and the DVN2 verdict; golden engine.golden.dvn4-one-cell-lights-up — a re-drawn 7-row table with one edit emphasizes one cell per side at the DEFAULT policy
DVN5Moved-code detection is explicitly out of scope for both waves (researched: VS Code / WinMerge prior art); the model must not preclude it (rows carry stable ids).not starteddeferred
DVN6Rendered-preview diff for markdown: diff the two MdDoc models (block-level alignment + inline text diff within blocks) and render change decorations in the decorated preview, with full block coverage from the first cut: tables cell-wise (changed cells tinted in the box-drawn table), paragraphs/headings/list items with inline word-diff tinting, code fences line-diffed inside the preview, callouts and nested lists; deleted blocks render collapsed/struck. The novel mode: --diff-preview.full (f86c66ca)apps/hue/src/md_diff.d aligns the two MdDocs (LCS over kind + normalized text, unmatched runs paired by similarity) into ONE tree the existing renderer draws: the new document's blocks with the removed ones spliced back in place, plus a flat decoration channel keyed by span start — the renderer's existing source-anchored identity. A container never carries its own verdict, its children do, which is what makes a re-aligned table contribute nothing. Table cells align by POSITION (a row's children are columns; content alignment would splice an extra cell into the row). Word emphasis reuses the engine's guarded LCS via refinePairTokens; fences diff by line. render_widgets applies a verdict once at the top of viewBlock by rewriting the options that already flow down the subtree — added takes the prose slot, removed takes it and strikes, changed arms the word ranges. Mode: --diff-preview. NOT done: the git-sourced and multi-file paths (the flag takes two markdown files), and collapsing a deleted block rather than striking it
DVN7Order-independent construct equivalence (tree-sitter-based): where the grammar has containers whose child order can be semantically irrelevant (class/struct members, overload sets, imports, attributes, markdown reference-link definitions), the structural pass (DVN3) must detect that a container's children were only permuted — every member matched 1:1 (by structural identity / a signature key) with no content change — and classify the reorder like DVN2 noise: demoted rendering with matched-pair move indicators, never a wall of remove+add. Whether an order is irrelevant is context-dependent — reordering D/C struct fields is an ABI/layout break, reordering imports or overloads is a trivial refactor — so commutativity must be a declarative per-language profile (Mergiraf LangProfile-style: which node kinds are commutative containers, matched by which signature key), shipped with conservative defaults and overridable per container kind via config/CLI. Distinct from DVN5: container-scoped permutation via structural matching, not general cross-file moved-code detection.full (17d0ad14)apps/hue/src/diff_commutative.d: declared CommutativeKind profiles (child node kind per language, since a D import_declaration commutes at module scope and inside a block alike), conservative defaults (D imports, markdown reference definitions — struct/enum members and parameters deliberately absent), --diff-commutative adds kinds or turns the pass off. A hunk is a reorder only when its two sides carry the same token MULTISET and a declared container was permuted inside it on each side — multiset equality alone would call a - bb - a a reorder. Its own Hunk.reordered flag, not formattingOnly: both fold, but the badge must not call a sorted import block formatting. NOT done: matched-pair move indicators (which member went where) — the fold names the verdict and zn expands to the rows

Type overlay & semantic diff (DVT)

Resolved D types on both sides of the diff, over the analyzer hue already spawns for a single open file (LIV*). The reviewer's questions this answers are the ones a text diff cannot: what is this expression's type after the change, did this signature change or only its body, does the new side still compile. Where DVN3/DVN7 are syntactic (tree-sitter), this layer is semantic (the type oracle) — no surveyed tool in the catalog resolves types on the removed side at all.

IDRequirementStatusTraces to
DVT1Both sides answer hovers. A .d file in a diff session attaches a lazy twoslash payload per side; pointing at an identifier on a removed row resolves it against the old revision and on an added row against the new one, through the existing LiveTypesSession protocol unchanged. Anchoring contract: the overlay attaches to a side only when the analyzed code is byte-identical to that side's text (the extractor runs the notation parser, and a DVS2 reconstructed old side may be null) — otherwise that side simply has no overlay. A mis-anchored popup is never acceptable.partial (5ab670fc)diff_view.TypeOverlay.attach (the byte-identity refusal) + per-row decorateCodeRow with the gutter offset; the workspace runs one LiveTypesSession per side for a two-file .d diff — GUI host half and hover popups pending
DVT2Revision provisioning by worktree (decision 13): each revision the session needs is materialized once as a detached git worktree under hue's cache root via sparkles:git's worktree verbs, shared by every file and side that references it, removed at session end and stale ones pruned at start. Hue's worktrees are named so git worktree list shows their provenance and are never confused with a user's branch worktrees (dman D9). The worktree side needs no provisioning (it is the repo); the index side (--staged) materializes with git checkout-index --prefix.not startedsparkles:vcs worktree slice; DVS3
DVT3Closure reuse (decision 14): one oracle per side-revision, not per file — it enumerates the modules its single analysis covers and serves lazy payloads and tips for any of them (EXT8), and hue caches payloads per (revision, path). Opening a file already covered by a live analysis must render its types without spawning anything; only an uncovered file starts a new process. Live oracles are bounded and the oldest is retired first.not startedEXT8; PRJ16 cache; proposed session-level oracle pool
DVT4Coarse verdict badges, session-wide (decision 15): every changed .d file carries a semantic verdict — public signatures changed / implementation only / doc only / type-preserving — derived by comparing an API-surface digest of each side (declared symbols with resolved signatures, attributes and effects, EXT9), not by per-identifier work. Badges render in the changed-file list (DVS4, TVU6) and in each file's header, and feed DVN2-style demotion: a file whose digest is unchanged is noise by the strongest available evidence.not startedEXT9 digest; DVS4; DVN2
DVT5Precise differential, on open (decision 15): for the opened file, identifiers paired across a changed row (DVM2/DVM4) whose resolved type or resolved symbol differs are marked with their own emphasis tier and their popup shows wasnow; identifiers that survive unchanged are explicitly quiet. This is the reviewer-facing payoff of DVT1 and it must never be computed for a file that is not open.not startedproposed type-pair comparison over DVM2 pairs
DVT6New-side diagnostics inline: the analyzer's error nodes for the new side render as below-line blocks in the diff (the twoslash error channel) — review-time typechecking, "this change does not compile", with the old side's diagnostics available for contrast so a pre-existing error is not mistaken for a regression.not startedanalyzer error nodes; OVL1 below-line channel
DVT7Provenance and degradation: every resolved tip states what it was resolved against (resolved at <rev>), so a type is never anonymous evidence. Non-D files, no twoslash-extract, no dub project, an unresolvable revision, a shallow clone missing the old commit, or a materialization failure leave the diff exactly as it renders today plus one notice — never a blank pane, never a modal, and never an "approximate" type silently substituted for a faithful one.not startedLIV4/PRJ15 degradation shape; totality
DVT8Sinks: hover popups in the TUI and GUI (one shared widget view, the SIG* layout); the HTML sink emits the same types as a static review page through the existing .twoslash-* pure-CSS :hover contract — both sides, no JavaScript (HTM9). ANSI (eager meta-lines) and Android (no analyzer on device) are explicitly out of the first cut.not startedrender_widgets.d; libs/twoslash render_html.d; HTM9

IMPORTANT

DVT requires render_widgets.d's per-line decoration application to be extracted from its payload-shaped entry point into a line-source-agnostic seam, so the diff's row builder can paint hover spans without duplicating the overlay's popup/underline logic. That refactor is the real structural cost of this area — and it is the same seam overlays.md needs for "twoslash stops being a mode", extended with per-side attachment (OVL8).

IDRequirementStatusTraces to
DVG1Hunk and file navigation: next/prev hunk and next/prev file keys in GUI/TUI; the explorer pane lists the session's changed files (DVS4) and clicking/selecting one jumps to it.full (2ac6dd0b)[/] walk the session's files, {/} step hunk to hunk (ViewerModel.diffMoveFile/diffMoveHunk, scrolling via each container's Widget.key), and activating an explorer row jumps to that file (diffSelectFile)
DVG2Unchanged-region collapsing: context beyond N lines folds with expanders (expand-up / expand-down / expand-all) — VS Code's hidden unchanged regions / Gerrit's context controls.full (678bf680)diff_view.contextGap bands each region with its line count; + opens the one in view and zx opens them all, as independent state. Expansion reads the side text — a patch without sources bands but cannot expand, by definition
DVG3Per-file sections collapse/expand in the multi-file view.full (ddf2decb)ViewerModel.diffToggleFile/diffSetAllFiles; z+key over a session folds files, collapsed files render a hunk count and skip re-highlighting
DVG4Search (FND/TUI search) works over the visible diff text on both sides.not startedexisting search + diff view
DVG5Performance: rendering is viewport-culled (RND1 analog) — scroll cost does not grow with diff size; the engine guards (DVM6) bound compute.full (23a17313)GridCanvas rejects a run whose row is outside the grid or any pushed clip before decoding it — paint cost went from linear in document length (0.54→22.8 ms over 200→12k rows) to flat (0.26→0.32 ms); the engine guards (DVM6) bound compute

Pull-request viewing (DPR) — second wave

| ID | Requirement | Status | Traces to | | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DPR1 | hue --pr <number\|url> must fetch a pull/merge request read-only through the forge seam (DPR7) and open it as a diff session. Fetch is native: the forge's REST/GraphQL API directly from D — no gh/glab binary dependency; the token is discovered per forge ($GITHUB_TOKEN / $GH_TOKEN, then gh's own config file as a courtesy; analogous sources per adapter), never prompted for interactively. On Android — no environment variables — the token comes from the config file (CFG12), documented as plain-text storage. | full (a677f13e) | --pr <number | url>: forge_client resolves the target against the checkout's remote (origin, then upstream), picks the adapter by host, discovers a token and fetches. Native over libcurl (std.net.curl) — no ghbinary; readinggh's hosts.yml is a config file, and hue never runs the tool. libcurl is opt-in per dub configuration (libs "curl"+versions "HueCurl"), so the Android and unittest builds link none and fetchHttpreportsunsupported instead of not existing. NOT done: the Android config-file token path (CFG12) | | DPR2 | A PR session = description (rendered through the markdown preview — dogfooding), metadata (author, state, branches, checks), and the file list as a DVS4 diff session. | full (7f512bef) | assemblePatch restores the diff --git/---/+++ preamble the forge omits, so the PR's files become exactly the input the engine's parser takes — a PR session IS a DVS4 diff session, same model, same noise layers, same renderers. DiffSession.header carries title/state/author/branches plus the description as an MdDoc, rendered through hue's own markdown view (the diff's DVM5 fence renderer doubles as the markdown view's, so a fence in a description is highlighted by the pipeline highlighting the diff below it). A session HEADER, not a PR header: nothing in it names a forge, so DPR3 and the W wave reuse it | | DPR3 | Review comment threads anchored to file+line render as inline blocks under their anchor line in the diff (Gerrit gr-diff prior art); resolved threads fold to a one-line badge. The thread-block widget is shared with local comments (DCM2). | full (1ef35569) | Threads fetched over GraphQL, not REST: REST carries no notion of a resolved THREAD at all, only comments and in_reply_to_id links — and resolution is what decides whether a conversation folds. That costs auth (GitHub's GraphQL refuses anonymous requests), so a tokenless session reads the diff with no threads, reported once. diff_session.AnchoredThread is forge-neutral and lives with the session, so DCM2's local comments produce the same value and no forge type reaches the renderer. Bodies are markdown through hue's own view; a resolved thread folds to a badge; a thread on the old side hangs on the line its author saw. Verified live on dlang/dmd#23530. NOT done: the split layout renders threads only in unified | | DPR4 | Revision comparison — diff between pushed revisions of the PR, surviving force-pushes (Reviewable prior art) — researched, not in the second wave's first cut. | not started | deferred | | DPR5 | Stacked-PR awareness (Graphite / av / git-spice / ReviewStack prior art) — researched only; this spec records the model but commits to nothing. | not started | deferred; research catalog | | DPR6 | Degradation: no network / no auth / unrecognized-forge remote must produce a clear error, never a crash; rate-limited fetches surface as such. | full (556b575c) | A closed ForgeErrorKind vocabulary (unknownRemote/noAuth/notFound/rateLimited/network/malformed/unsupported), because the user's next move differs per kind. The distinctions that matter and are tested: the same 404 means "log in" without a token and "you cannot see this" with one; GitHub answers a rate limit with 403, the same status as a permission denial. Targets that cannot resolve never become a request | | DPR7 | Forge interface (architecture): the PR session is built against a forge seam, not the GitHub API — a Design-by-Introspection adapter vocabulary where each forge type implements the core surface (resolve remote → repo, fetch PR/MR metadata + file list + threads) and declares optional capabilities by presence (revision timelines, suggestion syntax, draft reviews, stack metadata), probed by introspection à la git-spice's optional interfaces — never if (forge == …) branches in session/UI code. GitHub is the first adapter; GitLab, Gitea, Forgejo, Codeberg must be addable as adapters only. Forge detection from the remote URL (host mapping user-extendable via config, since self-hosted instances have arbitrary hosts); a capability a forge lacks degrades that one feature with an in-band notice. | full (556b575c) | apps/hue/src/forge.d is the seam and forge_github.d its first adapter: isForge!T states the core surface, hasCapability!(T, name) probes optional ones by PRESENCE (git-spice's pattern) so a forge without a capability lacks the member rather than stubbing it. The transport is injected (Transport), which is what leaves URL composition, pagination, decoding and every failure path under test without a socket; forge_client holds the three things that must touch the outside world |

Staging & editing (DST) — third wave

The write surface over the local diff session. Prior art: lazygit's pure patch model, gitui's hash identities, Meld's editable panes.

IMPORTANT

DST5, DCM3, and CFV4 all require a multi-line editable-text component that sparkles:ui does not have (hue's only text input today is the single-line search field) — cursor/insert/undo state machine, edit-aware layout, IME on desktop GUI, soft keyboard on Android, terminal input in the TUI. It is specced as UIA9 / docs/specs/ui/editor.md and is the critical path of the write wave: schedule it before W1/W2.

IDRequirementStatusTraces to
DST1Stable identities: every hunk and row carries a content-derived id (hunk-header hash + row content hash) so interactive state — staging selection, comment anchors, fold state — survives recompute: the UI remembers a hash, the backend re-derives the diff and re-matches (the gitui pattern).not startedproposed model ids; research: gitui
DST2Hunk/line staging: stage/unstage a selection from the worktree diff by synthesizing a zero-context sub-patch and applying it via git apply --cached --unidiff-zero (--reverse to unstage), preceded by a --check dry-run — no in-process index bookkeeping (the lazygit recipe).not startedproposed patch synthesis; research: lazygit
DST3Selection modes: LINE / RANGE / HUNK selection with both sticky-mark and shift-extend keyboard idioms, hunk mode as the default, and post-apply cursor restoration to the next change (the lazygit UX).not startedkeymap.d; existing selection machinery
DST4Discard (worktree-destructive): discard selected hunks/lines via reverse apply, always behind an explicit confirmation, never combined with a stage action in one keystroke.not startedproposed discard path
DST5Inline editing: the worktree side of a diff pane is editable in GUI/TUI (Meld's live-editable panes); edits re-diff live (debounced) against the fixed side; an explicit save writes the file; non-worktree sides are visibly read-only.not startedproposed editable pane; research: Meld
DST6Display vs apply separation: noise suppression and folding affect only what is shown — a staged or discarded patch always includes the hidden rows of the selected range (lazygit's forUI split) — and editing never auto-stages.not startedinvariant across DVN/DST

Comments & suggestions (DCM) — third wave

Line-anchored review annotations on any diff session — local-first, then submitted to forges.

IDRequirementStatusTraces to
DCM1Content-anchored comments: a comment anchors to file + row by content snapshot (anchor line plus neighbours), re-locating by content on every recompute — surviving re-diffs, rebases, and force-pushes (GitButler's but-comments durability model).not startedproposed anchor model; research: GitButler
DCM2Comments render as inline thread blocks under their anchor row — one widget shared with forge review threads (DPR3); resolved/archived threads fold to a one-line badge.not startedshared thread-block widget
DCM3Proposed suggestions: a comment may carry a range replacement authored by editing the anchored lines in place (DST5 machinery); it renders as a suggestion block with its own mini-diff.not startedDST5; proposed suggestion model
DCM4Suggestions serialize to two targets: the forge's suggestion syntax (GitHub's ```suggestion fence via the DPR7 capability) and a git-applyable patch for local review — an accepted local suggestion is applied with git apply. One model, two emitters.not startedproposed emitters
DCM5Review-state storage: draft comments, per-file viewed marks (file × revision — the Reviewable/diffy mechanism, enabling "diff since I last looked"), and fold state persist as JSON blobs in a versioned git ref (refs/hue/data, the git-spice pattern) — worktree-shared, history via commits, no working-tree pollution.not startedproposed ref store; research: git-spice
DCM6Review submission: queued comments/suggestions form a local draft review batch submitted atomically through the forge client (DPR1/DPR7); orphaned/outdated upstream threads are surfaced, never silently dropped (the diffy finding).not startedDPR7 capability; research: diffy

Conflict view (CFV) — third wave

3-way merge-conflict viewing and resolution.

IDRequirementStatusTraces to
CFV1Conflict-file ingestion: parse conflict-markered files (merge, diff3, zdiff3 styles, including OID labels from rebases) into a base/ours/theirs model reconstructed from the single file (Mergiraf's ParsedMerge precedent); enterable directly from a GitStatus.conflict file in the explorer.not startedproposed conflict parser; research: Mergiraf
CFV23-way layout: ours · base · theirs panes (base collapsible), rows aligned by the N-way pairing pass (DVM2); narrow widths degrade to a stacked per-conflict view (DVL3 analog).not startedDVM2 N-way; proposed 3-way widget view
CFV3Conflict navigation: next/prev-conflict keys, a conflicts-remaining count, resolved regions folding away; the explorer badges conflicted files (GitStatus.conflict exists today).not startedkeymap.d; git_status.d
CFV4Resolution: per-conflict pick ours / theirs / both (either order) plus free inline editing of the result (DST5); save writes the file with markers removed; optional staging via DST2; all picks undoable before save.not startedDST5; DST2; proposed resolution state
CFV5Structural resolution assist — commutative-aware auto-resolution (Mergiraf's class-based merge as an oracle marking conflicts "auto-resolvable", reusing the DVN7 profiles) — researched, deferred.not starteddeferred; research: Mergiraf

Milestones

Waves: V (viewing) → P (PR/forge reading) → W (write surface: staging, editing, comments, conflicts), with T (types) interleaved. Within V, noise precedes the split layout — the noise layers are layout-independent post-diff passes and they are the motivating pain.

The T wave is sequenced around one observation: T0 needs no git at all (two .d files on disk are both analyzable exactly as LIV1 analyzes one), so it can prove the anchoring contract, the dual-session shape and the render_widgets seam immediately, while everything expensive — worktree provisioning, closure serving, digests — waits for V2 to supply a real multi-file git session to hang it on.

MilestoneScopeStatusRequirements
V0sparkles:diff engine: line diff + alignment + patch parser + word refinement + guards + goldenspartial (440ab1f7)DVM1DVM8
V1Diff content kind in hue: two-file + piped-patch sources, unified layout, all four sinkspartial (31fdab59)DVS1, DVS2, DVS5, DVL1, DVL4DVL7
V2Git revisions + multi-file session + explorer integration + hunk/file navigationfull (2ac6dd0b)DVS3, DVS4, DVG1, DVG3
T0Type spike: --diff a.d b.d, both sides on disk — dual sessions, anchoring, TUI/GUI popups (no git; runnable before V2)partial (5ab670fc)DVT1, DVT7; the render_widgets seam
T1sparkles:git worktree verbs + revision provisioning + closure-serving oracle → types on git-sourced diffs (after V2)not startedDVT2, DVT3; EXT8; dman D16
T2API-surface digests → session-wide verdict badges in the explorer + inline new-side diagnosticsnot startedDVT4, DVT6; EXT9
T3Precise per-identifier differential (was → now) + the static HTML type-review pagenot startedDVT5, DVT8
V3Noise layers 1–2: whitespace toggles + formatting-only classification + table-cell goldenfull (1b96f740)DVN1, DVN2, DVN4
V4Split layout + runtime toggle + width degradation + unchanged-region collapsingfull (678bf680)DVL2, DVL3, DVG2, DVG5
V5Structural pass: classification oracle + opt-in structural view + commutativity profilesfull (17d0ad14)DVN3, DVN7
V6Rendered-preview diff for markdown (full block coverage)full (f86c66ca)DVN6
P0Forge seam + GitHub adapter; PR session: description, metadata, file-list difffull (a677f13e)DPR1, DPR2, DPR6, DPR7
P1Inline review-comment threadsfull (1ef35569)DPR3
W0Staging: stable ids, hunk/line stage/unstage, selection modes, discard, display/apply invariantnot startedDST1DST4, DST6
W1Inline editing of the worktree side (requires the UIA9 editor component first)not startedDST5; UIA9
W2Local comments: content anchors, thread blocks, refs/hue/data store + viewed marksnot startedDCM1, DCM2, DCM5
W3Suggestions: authoring + both emittersnot startedDCM3, DCM4
W4Review submission (draft batch → forge)not startedDCM6
W5Conflict view: ingestion, 3-way layout, navigationnot startedCFV1CFV3
W6Conflict resolutionnot startedCFV4

Relationship to existing specs

PieceRole
feature-requirements.md MOD/SRCthe dispatch collapse + session substrate diff extends
ui-architecture.mdthe shared widget view both interactive sinks paint
tree-view.md / explorer.dchanged-file navigation pane
folding.mdthe fold mechanics DVG2/DVN2 reuse
gallery.md GAL7 / HTM8HTML selection domains for split panes
sparkles:syntaxhighlighting both sides (DVM5); the ts engine behind DVN3
twoslash.md LIV*the live-types oracle the DVT overlay attaches per side
sparkles:dmd-lsp EXT8/EXT9closure-serving oracle + the API digest behind the badges
sparkles:git (dman D16) / dman D9the worktree verbs that provision the old revision (DVT2)
docs/research/diff-review/the prior-art survey grounding every "prior art" claim above

Overview · Feature requirements · Tree / DAG view