Skip to content

feat: give the built-in demo real diffs, and finish the interface pass - #8

Merged
renrenmimi merged 6 commits into
mainfrom
feat/real-demo-diffs-and-polish
Sep 3, 2026
Merged

renrenmimi merged 6 commits into
mainfrom
feat/real-demo-diffs-and-polish

Conversation

@renrenmimi

@renrenmimi renrenmimi commented Sep 3, 2026 •

Copy link
Copy Markdown
Owner

The built-in demo is the first thing most visitors see, and until this branch it
could not show what the application is for. Expanding next.config.ts or
app/layout.tsx reached:

This source did not provide a diff for this file. That is not the same as the
file being unchanged.

That is an honest message about a live repository that withheld a patch. In the
application's own demo it is a dead end. This branch fixes that, and then makes
a pass over the three views, the visual system and the responsive and keyboard
paths.

Why there was no diff

The fixture stored hand-written hunks next to hand-written line counts, and
for most files it stored no hunk at all — only additions, deletions and a
status. So a row said +8 and then had nothing to show. Two further problems
followed from the same arrangement: the counts and the patch could disagree with
each other, and Compare could not compute a true net difference between two
arbitrary commits because it had no content to diff, so most rows reported
changed more than once and showed only summed numbers.

How it generates real diffs now

The fixture stores file content, not diffs. Each commit lists operations —
add, mod, del, ren — carrying the file's full text at that commit.
Nothing about a change is written down twice:

Shown in the interface Where it comes from
The unified diff diffText(previousText, newText)
+n / −n on a file Counted from the hunks that diff emitted
Commit totals Summed from the same per-file numbers
File size The byte length of the stored text
Object id A hash of the stored text, so identical bytes compare equal
Compare's net diff diffText between the two commits' stored texts

src/lib/diff/unified.ts is the diff engine: a longest-common-subsequence edit
script with common prefix and suffix trimmed, three lines of context, and
diff -U3 hunk headers. About three hundred lines, no dependencies, and it also
produces the two-column view so the unified and split renderings cannot disagree
about what changed.

Result, across the sixteen commits:

  • 121 changed files open on a real unified diff.
  • 3 are the withheld generated ones — package-lock.json (twice) and
    public/generated/search-index.json. Their rows are marked generated
    before you expand them; expanding explains why the content is not carried
    and says their line counts are declared, not measured from a diff.
  • No expandable row dead-ends, and not-provided never appears in the demo.
  • All 256 ordered pairs of commits compare correctly — no aggregated, no
    missing patch, and a reversed pair swaps base, head, additions and deletions.

The demo keeps dataSource: builtin, htmlUrl: null, exactly 16 commits, and
no real repository names, commit hashes or project history.

The 0-GitHub-requests proof

Three independent arguments, all in CI:

  1. fetch is replaced by a throw. tests/builtin-demo-diffs.test.ts builds
    the demo, serves every commit, projects every tree and runs all 256
    comparisons with globalThis.fetch set to a function that throws. Everything
    passes, so nothing in serving the demo reaches the network.

  2. The adapter is unreachable. An import-graph walk from the demo provider
    proves no path leads to src/lib/github/. It cannot be called by accident.

  3. Measured in a browser. Against this production build, one full repository
    view of the demo:

    Step Requests to this application To github.com
    Initial load, fully settled 45 — repo 1, commits 1, commit 16, tree 16, tags 1, probe 10 0
    Sub-views, opening a diff, Insights, theme twice, back, one step +0 0
    Opening Compare (runs the proposed pair once) +1 0
    Moving both selectors and pressing Swap +0 0
    Pressing Compare +1 0

    45 is the same initial figure as main; no request was added by any of this.
    The requests it does make are to this application's own routes, which serve
    the demo from memory.

The rest of the pass

Replay. History is 296px with a two-line subject and a stable four-column
grid for number, date, sha and marker. The commit header re-establishes its
hierarchy — the title is the visual centre, and Built-in demo,
0 GitHub requests and the disclosure moved off the H1 line into one quiet
metadata row. The player is one bounded region: Previous/Play/Next,
Latest and the speed setting sit in a single cluster instead of leaving a hole
across the middle. Diffs read unified by default, with an optional two-column
view at ≥1280px; narrow screens are always unified.

Compare is unchanged in interaction, as intended. It benefits from the
fixture work: every row now opens on a real net diff.

Insights. Milestones collapse to five with Show all milestones in the
section heading rather than under the list — the old placement moved as the list
grew, and the browser scrolled to follow it, throwing the reader to the bottom
of the page.

Three real bugs the audit found, each with a test:

  • Leaving Replay for Compare and coming back reset the sub-view, emptied the
    path filter and collapsed the open diff, because all of it was local state in a
    component that unmounts. It now lives in the shell. (Stepping to another commit
    still collapses the diff — those are different files.)
  • Opening the narrow-screen history drawer while scrolled jumped the page to the
    top behind it: the overlay captured window.scrollY after focusing its first
    control, and that focus had already scrolled the page. Now captured first, with
    preventScroll on both focus calls and a layout flush before restoring. Zero
    drift at scroll offsets 0, 120 and 200.
  • A reversed comparison diffed forwards, because netFiles keyed its from sha
    off the direction flag. It always diffs base → head now.

Visual system. The palette is applied exactly as specified, in all three
token blocks. tests/contrast.test.ts now reads the shipped stylesheet and holds
54 pairs per theme — 108 in all — to 4.5:1 for text and 3:1 for borders, focus
rings, chart strokes and categorical dots. All pass; the tightest are 4.51:1 and
4.71:1. The same file asserts both themes declare the same token names and that
the prefers-color-scheme fallback is value-for-value identical to the explicit
dark theme.

Verification

Check Result
npm run typecheck clean
npm run lint clean
npm test 792 passed (was 679 on main)
npm run build clean
npx playwright test 248 passed — desktop, Pixel 7, reduced-motion
Token canary, fake token no canary in any page, header, or the 11 loaded chunks; tokenConfigured=true is the only credential fact exposed
Source integrity suite clean
Horizontal overflow none at 1440, 1280, 1024, 390, 360 and 320% zoom, in both themes
State matrix empty repo, 1 commit, 5 commits, 300-of-640, 404, private, rate-limited, unknown demo, network failure — every one has clear next steps, and rate-limited still offers the quota-free demo

Screenshots are in docs/screenshots/review-*.jpg: home, Replay commit 1,
Replay commit 2, a real diff, a withheld generated file, the two-column diff,
Compare, a Compare net diff, Insights, both themes, 1280×720, and 390px for the
replay, the drawer, Compare and a diff. Every one comes from the built-in demo
or the synthetic fixtures — none from a real repository.

Walking the brief's path against a production build of this branch — Explore the
demo, Commit 1, Next to Commit 2, Repository, Changes, expand next.config.ts,
expand app/layout.tsx, Compare, Insights, back to Replay — lands on Commit 1
of 16 paused, shows a real hunk for both files, reads
f195117 is 1 commit ahead of efc489e. 8 files changed, 1,908 lines added and 1 removed., finds Insights with its milestones collapsed, and comes back to
Commit 2 with the Changes sub-view and the open diff intact. 0 requests to
github.com, 0 console errors.
(The Vercel preview for this branch is behind
deployment protection, so the walk ran against a local production build of the
same commit; I will walk public production after the merge.)

No feature was removed, nothing became a three-column dashboard, and there is no
generated summary anywhere.

🤖 Generated with Claude Code

renrenmimi and others added 5 commits September 3, 2026 11:46
Expanding almost any file in the demo answered:

    This source did not provide a diff for this file.
    That is not the same as the file being unchanged.

Measured against production: of the 122 changed-file rows across the sixteen
commits, **16 had a diff and 106 did not**. That sentence is an honest fallback
for a live repository, which cannot be asked twice. For data the application
ships itself it only ever meant "the fixture was never finished" — and the demo
is the first thing a visitor sees.

Content is now the source of truth
----------------------------------

The fixture used to carry hand-written hunks for a handful of files and
hand-written line counts for all of them. It now carries the **complete text of
every file at every revision**, and the provider derives the rest:

- the patch, by diffing the previous revision against the new one;
- `additions` and `deletions`, counted from that patch's own hunks;
- the tree entry's size, from the bytes;
- the blob id, from the bytes — content-addressed, as Git is, so a file that
  returns to earlier content correctly compares as unchanged.

None of those can drift from each other, because none of them is written by
hand. Result: **121 real unified diffs, 0 dead ends.**

`src/lib/diff/unified.ts` is the diff itself — a trimmed-prefix LCS producing
`diff -U3` output, about 200 lines and no new dependency. It is covered by a
200-seed property test that applies each patch back onto its input.

Comparisons compute the difference instead of reassembling it
------------------------------------------------------------

`compareLocalHistory` gained an optional `contentAt`. When a source can produce
file content — the demo can, for every commit — the net difference between two
points is computed from the two versions rather than reused from a single
intervening hunk. So a file touched by three of fifteen commits now shows its
actual net diff, where before it showed `aggregated` and a summed line count.

All 256 ordered pairs of the demo's commits are asserted to produce a net diff
with no `aggregated` and no `not-provided` anywhere.

That also fixed a bug this introduced in passing: the direction was read from
`forward` rather than from the requested endpoints, so a reversed comparison
diffed the wrong way round. A comparison always describes head relative to base.

Files that are deliberately not shown
-------------------------------------

Two are withheld: `package-lock.json` and `public/generated/search-index.json`.
Both are machine-written, and a lockfile diff teaches a visitor nothing about
how a history grew.

They no longer dead-end:

- the row is marked `generated` *before* it is opened, from the path, using the
  same classifier the file tree uses, so the two always agree;
- opening it explains that the diff is withheld on purpose, and says the line
  count is the size the fixture *declares* rather than a count taken from a
  diff — because there is no diff to take it from;
- `patchOmittedReason` gained `generated` and `rename-only`, so each of these is
  its own sentence instead of collapsing into the generic one.

`generated/` also joins the classifier's list of machine-written directories,
which is as common a convention as `dist/` and now benefits live repositories too.

What did not change
-------------------

Sixteen commits, the same shas, subjects, bodies, dates, tags and file paths.
`dataSource: builtin`, `htmlUrl: null`, no blob URLs, and **0 requests to
github.com** — asserted by running the whole demo with `fetch` replaced by a
throw, and by walking the provider's transitive imports to prove the GitHub
layer is not even reachable from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts rhythm

A polish pass over the structure the previous round established. Nothing was
removed and no view was restructured.

Palette
-------

`src/styles/tokens.css` now carries the specified scale exactly — page
`#F7F8F5` on `#FFFFFF` with `#EEF3EE` behind secondary regions, accent `#2F7657`,
and separate `added` / `removed` / `warning` pairs — with a dark theme built as
its own scale rather than an inversion.

Measured across every text role on every surface it is used on, in both themes:
**41 of 41 pairs pass** (4.5:1 for text, 3:1 for borders, focus rings and chart
strokes). The tightest is `removed` on `removed-soft` at 4.51:1, which is the
specified pair and passes with little to spare; it is left exactly as specified
rather than quietly adjusted.

Replay
------

- The source line has a **row of its own** under the repository's name. Sharing
  the heading's line, `Built-in demo · 0 GitHub requests · This is a curated,
  synthetic history…` made the title read as a sentence rather than a title.
- The player is **one bounded region**: previous, play, next, Latest and speed in
  a single cluster with the track beneath them. Speed pinned to the far right
  left a hole across the middle of the row that read as a break in the layout.
  Play stays the only filled control; previous and next are the same height
  without the fill; speed is compact, as a third-level setting.
- The commit heading has a clearer hierarchy: position in the smallest type, the
  subject at 22px as the visual centre, author and sha as 13px metadata with the
  sha in monospace so it reads as an identifier.
- The history column is **296px** and gives each subject **two lines** instead of
  truncating after a few words. Rows stay a fixed 80px so the virtualiser's
  arithmetic is still exact, and the number, date, sha and marker sit on a
  four-column grid so they hold their positions down the list. The full subject
  is in the tooltip and in the row's accessible name.
- Changed-file rows use fixed columns for the status word and the line counts, so
  a row carrying a `generated` badge no longer pushes them out of line.

Side by side
------------

Above 1280px the diff can be read in two columns; below it, and on every phone,
unified is the only option — two columns of code at 390px is one unreadable
column twice. `toSideBySide` derives the layout from the same hunks the unified
view renders, pairing a replaced line onto one row, so the two cannot disagree
about what changed. Every cell is a text node, as before, and the box scrolls
inside itself.

Insights
--------

The milestone list opens at five commits with `Show all milestones` beside the
heading, so Growth and the scope statement still fit the first screen.

The control is in the heading and not below the list on purpose: below it, the
button moved down the page as the list grew and the browser scrolled to follow
the element it had just focused — which threw the reader to the bottom of a
section they were part-way through. An end-to-end test now measures that neither
the first row nor the control moves when the list expands.

Home
----

The two entries are equal width and end on the same line. The demo keeps the
only filled button on the screen and needs no input, so it is still the quicker
path; sizing it as the smaller card made it look like the lesser option instead.
The privacy note is one sentence, and the message slot under the field reserves
its height so an invalid entry replaces the hint in place — asserted by
measuring the layout before and after a rejection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things a visitor chose were being thrown away.

Returning to the replay lost where you were
-------------------------------------------

`ReplayView` unmounts when the visitor switches to Compare or Insights, and it
held its own sub-view, its diff focus and the file tree's filter, expansion and
selection. So the trip back reset the sub-view to Repository, emptied the path
filter and closed every folder that had been opened to get somewhere.

All of it now lives in `TimeMachine`, which does not unmount, and the tree panel
takes it as a controlled prop. Opening a different repository clears it, because
a different repository has a different tree.

An expanded diff still collapses, and that is deliberate: the sub-view and the
filter are where you were, an open patch is what you were reading, and a list of
collapsed files is the right thing to come back to. There is a test saying so,
so it reads as a decision rather than an oversight.

Opening the history drawer jumped the page to the top
-----------------------------------------------------

`Overlay` captured the scroll position *after* focusing its first control. That
control sits in a `position: fixed` surface at the top of the viewport, so
focusing it scrolled the page to 0 — and the position then captured was 0. The
page behind the drawer jumped to the top, and closing it restored the jump.

Now the position is read before anything inside is focused, both focus calls pass
`preventScroll`, and the restore forces a layout read before scrolling: while the
body is fixed the document is only as tall as the viewport, so scrolling before
it has been re-measured gets clamped to whatever fits.

Measured at four offsets, opened by dispatching the click so the harness is not
scrolling the button into view and measuring itself: locked at exactly the offset
it opened at, restored to exactly that offset, zero drift. An end-to-end test
holds it there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ME claims

Four small things the screenshot pass turned up.

The demo card carried a title, a button and a cost line, while the card beside
it carried a field and two paragraphs of help. Stretching both to the same
height left a hole in the middle of the demo card. It now opens with the same
sentence the demo itself uses, which fills the space with something worth
reading and gives the two entries equal weight.

Hover and selection were within 0.01 of each other in luminance and both
green-tinted, so the pointer tinted a row as strongly as the current commit did
and the history read as having two current rows. Hover is now clearly fainter
and less green. Selection keeps its accent bar, so it stays the only row that
looks chosen. (`--bg-hover` is ours, not one of the palette values fixed by the
brief.)

Insights measured 960px while Compare measured 1080px, for no reason anybody
could see. They now agree.

An empty repository offered only "Change repository". Somebody who arrived to
look at a history should be offered one, so it now also offers the demo — the
same escape hatch every other failure state gives.

The contrast claim in the README was measured once, by hand, which is how such
a claim quietly stops being true. tests/contrast.test.ts reads the shipped
stylesheet, composites translucent values over their backdrop, and holds 54
pairs per theme to 4.5:1 for text and 3:1 for borders, rings, chart strokes and
categorical dots. It also asserts that both themes declare the same token names
and that the prefers-color-scheme block is value-for-value identical to the
explicit dark theme, so nobody can end up with two different dark palettes.

All 108 pairs pass. The tightest are removed-line red on its own tint at 4.51:1
in light, and muted text on the selected row at 4.71:1 in dark.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ages

The README described a fixture that stored diffs. It now describes the one the
application ships: file operations carrying full text, with the patch, the line
counts, the file size and the object id all derived from that text. A table says
which shown number comes from which computation, and the section says plainly
what was wrong before and why deriving everything from one source fixes it.

Also corrected or added:

- Compare's numbers section now says the demo computes true net diffs from
  content at both ends, so the "changed more than once" fallback — which is
  still the honest answer where only diffs are available — never applies to it.
- The accessibility section points at tests/contrast.test.ts and quotes the
  measured figures rather than an older hand count.
- The commit history's virtualised row height was still documented as 64px.
- The replay section says that state survives leaving the view, and that diffs
  read as unified by default with an optional two-column view on wide desktop.
- The Playwright section lists what the suite now asserts about the route a
  first-time visitor takes.

The screenshot script no longer reports the deliberate 429 on the rate-limit
shot as a problem; a shot of a failure state has to provoke the failure. Any
other console error on that page is still reported.

docs/timeline.jpg and the committed review images are regenerated from the
built-in demo and the synthetic fixtures only, at the current palette and
layout. The set now covers a real diff, a withheld generated file, the
two-column diff, a Compare net diff, both themes, 1280x720, and 390px for the
replay, the drawer, Compare and a diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 3, 2026 19:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
repo-time-machine Ready Ready Preview Sep 3, 2026 7:57pm UTC

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 165f828ddd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/compare/local.ts
Comment on lines +183 to +184
additions: forward ? (touch?.additions ?? 0) : (touch?.deletions ?? 0),
deletions: forward ? (touch?.deletions ?? 0) : (touch?.additions ?? 0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compute opaque-file totals from the comparison endpoints

When a comparison spans more than one change to an opaque file, these values remain cumulative touch totals rather than a net difference. For example, comparing the demo's first commit with its last reports package-lock.json as +2,008/−28 (the +1,840 creation plus +168/−28 modification), even though the base lacks the file and the head declares a 1,980-line file, so the correct net result is +1,980/−0. This also corrupts the comparison-wide line totals while the new generated-file fallback presents the figures as endpoint-derived; use the stored endpoint line counts for additions/removals or explicitly retain the aggregated limitation.

Useful? React with 👍 / 👎.

Comment thread src/lib/diff/unified.ts
Comment on lines +287 to +291
} else {
flush();
rows.push({ kind: 'context', oldNumber, newNumber, text: body });
oldNumber += 1;
newNumber += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle no-newline markers before treating lines as context

In the side-by-side layout, a GitHub patch containing \ No newline at end of file reaches this fallback branch. The marker is then rendered as unchanged source on both sides and both line counters are incremented, making any following context or changes in the same hunk display incorrect line numbers. Recognize this backslash-prefixed diff annotation as metadata and do not advance either counter.

Useful? React with 👍 / 👎.

Comment on lines +62 to +64
const [subView, setSubView] = useState<SubView>('repository');
const [diffFocus, setDiffFocus] = useState<{ path: string } | null>(null);
const [treeView, setTreeView] = useState<TreeViewState>(EMPTY_TREE_VIEW);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset lifted replay state on URL-driven repository changes

These values now survive ReplayView unmounts, but they are reset only by openRepo; the popstate path calls applyUrl and loads another repository without clearing them. After configuring a filter or selecting Changes in repository B and using Back to return to repository A, A inherits B's subview, filter, folder overrides, and selected path, potentially showing an empty or unexpected workspace. Reset this state whenever a URL load changes the repository, not only for picker-driven loads.

Useful? React with 👍 / 👎.

This reverses a decision made earlier on this branch. Walking the brief's own
path — open a diff, look at Compare, come back — the collapse that commit 36aae0f
called deliberate did not read as deliberate. It read as having lost your place.

The argument for collapsing was that a patch is what you were reading rather
than where you were. Two things are wrong with it. Getting back to the patch
costs a scroll and a click, which is exactly the friction the sub-view and the
filter are preserved to avoid. And collapsing the diff reflows the list, so the
scroll position that *is* preserved lands somewhere else — the two behaviours
worked against each other.

`FileChangeList` now takes the open diff as an optional controlled prop, and the
replay path passes it down from `TimeMachine`. Compare keeps the uncontrolled
default, because its result already lives above the list.

The state stays keyed by commit, so stepping to another commit still collapses
it: those are different files, and following the playhead onto a path that may
not exist there would be worse than closing it. Two tests now, one for each half
of that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@renrenmimi
renrenmimi merged commit 7b9af51 into main Sep 3, 2026
4 checks passed
@renrenmimi
renrenmimi deleted the feat/real-demo-diffs-and-polish branch September 3, 2026 20:02

This branch was successfully deployed

1 active deployment
Preview — 0e63414a Deployed Sep 3, 2026 by vercel[bot]
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.

2 participants