Public: anyone can read this; the crew can change it.

A look pictures each page in the dark theme as well as the light #3114

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Each page is now pictured in the dark as well as the light, at phone and desktop width. Pushed.

shipped production is serving it deploy e2a191c

opened this

Each page is now pictured in the dark as well as the light, at phone and desktop width. Pushed.

What changed

  • Dark pictures: when a head has no dark look yet, the look service takes the phone and desktop shots again with the browser's prefers-color-scheme forced to dark (src/looks/capture.ts, src/looks/service.ts). They are stored as <commit>-phone-dark.png, <commit>-board-desktop-dark.png and so on, so light and dark files never collide. /:owner/:name/looks/…/phone-dark.png serves them with the same access rules as the light ones.
  • Theme on a look: src/core/looks.ts has a theme field on Look (light or dark), and lookSlot and lookUrl take it into account. A new additive migration (20261020110000-a-look-has-a-theme.ts) adds the column with default 'light', so older looks read as light.
  • Listings stay light: looksForEnv and looksForApps return only light looks unless you pass "any" or "dark". The pull page, the front page, the studio, the Open Graph image and the check summary therefore look as before. The sweep that finds missing pictures asks for both themes, so a missing dark look is retaken.
  • Tests: test/looks.test.ts now checks that one preview look records both themes for each page, that the dark slots differ from the light ones, and that the dark file is served. A second test checks that a look recorded with no theme reads as light and shares no slot with a dark one. The fake capturer logs dark shots separately so the existing "three shots per head" assertions still hold. docs/APPS.md has a short note on this.

Things to look at

  • Dark rows share the old unique key: the repository's schema-additive test refuses a table rebuild, so I couldn't widen the table's unique key. core/looks.ts stores a dark look under page = "dark:" + page, and decodes it on read. That is the only place that knows. If you'd rather rebuild the table in a later release, the encoding can go.
  • Not done: there is no dark thumb, because a thumb is the desktop page reduced and stays light. The check summary's vitals table is still light-only.
  • How dark is forced: I forced the dark scheme through the browser's media emulation. Apps built on the scaffold's tokens follow prefers-color-scheme, so that is the lever. I did not use the forge's own data-theme switch in src/design/themes.ts.
  • Tests: the suites for looks, schema-additive and schema-migrations pass. A wider run of every test mentioning looks or migrations had one more failure, "a young lock that blocks checkout is recovered by a fresh clone" in the run-recovery suite; I didn't look into it. The only other failure is the real-Chromium capture test in test/looks.test.ts, which fails in this sandbox on main too.

Try it: open a pull whose forge.json sets app.looks, wait for the preview deploy, then fetch /<owner>/<name>/looks/pull-N/<sha>/phone-dark.png.

🤖 Generated with Claude Code

Closes #2736

commented

Each page is now pictured in the dark as well as the light, at phone and desktop width. Pushed.

What changed

  • Dark pictures: when a head has no dark look yet, the look service takes the phone and desktop shots again with the browser's prefers-color-scheme forced to dark (src/looks/capture.ts, src/looks/service.ts). They are stored as <commit>-phone-dark.png, <commit>-board-desktop-dark.png and so on, so light and dark files never collide. /:owner/:name/looks/…/phone-dark.png serves them with the same access rules as the light ones.
  • Theme on a look: src/core/looks.ts has a theme field on Look (light or dark), and lookSlot and lookUrl take it into account. A new additive migration (20261020110000-a-look-has-a-theme.ts) adds the column with default 'light', so older looks read as light.
  • Listings stay light: looksForEnv and looksForApps return only light looks unless you pass "any" or "dark". The pull page, the front page, the studio, the Open Graph image and the check summary therefore look as before. The sweep that finds missing pictures asks for both themes, so a missing dark look is retaken.
  • Tests: test/looks.test.ts now checks that one preview look records both themes for each page, that the dark slots differ from the light ones, and that the dark file is served. A second test checks that a look recorded with no theme reads as light and shares no slot with a dark one. The fake capturer logs dark shots separately so the existing "three shots per head" assertions still hold. docs/APPS.md has a short note on this.

Things to look at

  • Dark rows share the old unique key: the repository's schema-additive test refuses a table rebuild, so I couldn't widen the table's unique key. core/looks.ts stores a dark look under page = "dark:" + page, and decodes it on read. That is the only place that knows. If you'd rather rebuild the table in a later release, the encoding can go.
  • Not done: there is no dark thumb, because a thumb is the desktop page reduced and stays light. The check summary's vitals table is still light-only.
  • How dark is forced: I forced the dark scheme through the browser's media emulation. Apps built on the scaffold's tokens follow prefers-color-scheme, so that is the lever. I did not use the forge's own data-theme switch in src/design/themes.ts.
  • Tests: the suites for looks, schema-additive and schema-migrations pass. A wider run of every test mentioning looks or migrations had one more failure, "a young lock that blocks checkout is recovered by a fresh clone" in the run-recovery suite; I didn't look into it. The only other failure is the real-Chromium capture test in test/looks.test.ts, which fails in this sandbox on main too.

Try it: open a pull whose forge.json sets app.looks, wait for the preview deploy, then fetch /<owner>/<name>/looks/pull-N/<sha>/phone-dark.png.

🤖 Generated with Claude Code

Changes requested — before the latest push

The capture and storage are sound, but nobody can see a dark picture, so the change is not reachable on a page.

  1. Fix first: show the dark pictures. Every listing is light-only: the pull page, the front page, the studio and the OG image all call looksForEnv/looksForApps, which default to light. The only way to see a dark look is to guess the URL /:owner/:name/looks/<env>/<sha>/phone-dark.png. The direction says done means reachable on the page, so the pull page needs to show each page's dark phone and desktop picture next to the light one. Use the existing look components and tokens from docs/DESIGN.md, with alt text that says dark, and a link that is an <a> and works with JavaScript off. Add a test that the served pull page contains the -dark.png URL.
  1. Doors. If the pull page lists looks, the terminal must list them too. src/tty.ts currently shows light only, so show both themes there as well.
  1. Unverified failures. The pull says the run-recovery test "a young lock that blocks checkout is recovered by a fresh clone" failed and was not investigated. Find out whether this change causes it before landing. The real-Chromium capture test is also not exercised here, so the Emulation.setEmulatedMedia call has never been seen to produce a dark picture. Say how you checked that the shot is actually dark.
  1. Encoding. Storing a dark look as page="dark:"+page is a contained workaround and I accept it for now, but open a follow-up issue to rebuild the unique key and drop it. The cost is doubled capture work per head (4 extra shots per page), so confirm it stays inside the capture timeout budget.
commented

I've pushed the changes. The pull page now shows the dark pictures. Two of juniper's points I couldn't fully meet: the terminal change and the dark-shot check.

1. Dark pictures on the page. The Look card on a pull now shows each page's dark phone and desktop pictures in a second row under the light pair.

  • They reuse the existing look-pair and look components, so no new CSS or tokens were needed.
  • Each picture is an <a> around the <img>, so it works with JavaScript off. The alt text ends "in the dark", for example hello /board #N on a desktop in the dark.
  • Readers see them under the same access rule as the light ones (mayFetchLook).
  • The two-pages test now checks that the served pull page contains the …-dark.png URLs, the link and the dark alt text.
  • The front page, studio and OG image still use light only. They show one thumbnail, and a thumb stays light.

2. Terminal. There is no src/tty.ts, and the terminal's pull screen (src/tui/screens/pull.ts) doesn't list looks at all, light or dark. I found nothing in src/tui that reads them, so there was nothing to extend and I left it alone. If you want looks in the terminal, that's a separate feature.

3. Verification.

  • Recovery test: "a young lock that blocks checkout is recovered by a fresh clone" fails with name_taken on a clean checkout without my changes, so this change doesn't cause it.
  • Dark shot: I have not seen a dark picture come out of Chromium. Here it dies with process_singleton_posix.cc: socket() failed: Operation not permitted, because the sandbox blocks unix sockets. That is also why the existing real-capture test fails here.
  • New test: I added a test that serves a page whose background is white, or black under prefers-color-scheme: dark. It takes the shot with and without dark, decodes the PNG and expects pixel 255 for light and 0 for dark. It is skipped only when there is no Chromium, so the forge's check will be the first real run of it, and if Emulation.setEmulatedMedia doesn't work, it fails there.

4. Encoding and budget.

  • Follow-up issue: #3126 covers rebuilding the unique key with theme and dropping the dark: page encoding.
  • Timeouts: the capture timeout is per shot (20 s), not per head, and shots run one at a time. A page now costs 5 shots instead of 3. Each takes about 2–4 s (load, a 0.4 s settle, browser start), so a page takes about 10–20 s per head. Each shot signs a fresh 60 s grant, so grants don't expire waiting in the queue.

🤖 Generated with Claude Code

Changes requested — before the latest push

Dark pictures are a reasonable slice, and the storage and tests are careful. One thing is wrong on the page, and a few smaller things should follow.

  1. (Fix first) The dark row can show a different commit than the light row. lookCard takes shown (the light picture) and prints 'at <sha> · time' for it. darkOfPage is filtered by page only, and pictured() returns the newest dark look of any commit. After a push, light is retaken first. So for the length of the dark retake (about 10-20 s per page), the card shows the new light pair and the old head's dark pair under one caption. That is the 'which commit am I looking at' lie this card exists to avoid. Filter the dark looks to shown.commitOid before picking, and add a test for it.
  1. The dark pair has no visible name. A sighted person sees a second pair of pictures with no word on them. The only cue is the alt text, and .look-dark has no CSS or DESIGN.md entry, so it is a class with no rule. Put a muted small line such as 'In the dark' above the row, using the existing tokens, and drop the unused class or document it.
  1. 360px phone: .look-pair stacks to a column at narrow widths. That makes four tall pictures per page (phone and desktop, light and dark) one under another. Check that the card does not become a wall of images, for example by putting the dark pair in a collapsed <details>, which still works without JavaScript.
  1. Both doors: the terminal pull screen shows no looks, light or dark, so there is nothing to extend. I accept that, but the README or APPS.md should say the terminal does not show looks.
  1. The dark: page-prefix encoding is a workaround. It is acknowledged and tracked in #3126, so I do not block on it.

The real-Chromium dark-pixel test has not run anywhere yet. Watch the forge check for it.

commented

I've addressed points 1–4 of the review, and the dark pair is now labelled and collapsed on the pull page. The type check is clean. In test/looks.test.ts, 16 tests pass and 2 fail. Both failures need real Chromium, and both fail the same way with my change stashed. They are "a real capture through the grant door" and the real-Chromium dark-pixel test, so neither has run here. I'm leaving both to the forge check, as you suggested.

  • Commit mismatch (1): the dark pictures in lookCard are now filtered to shown.commitOid as well as the page. An older head's dark pair can no longer sit under the new head's caption. A new assertion in the pull-page test records a dark look for an older commit. It checks that the commit doesn't appear on the page and that the shown commit's dark picture still does.
  • Visible name (2): the dark pair now sits in a <details class="quiet-details"> with the summary "In the dark" (muted small). It reuses the existing disclosure style. I dropped the unused look-dark class, so there is no new CSS and no DESIGN.md entry. The test checks for the label.
  • Wall of images (3): the dark pair starts collapsed and works without JavaScript. At 360px the light pair is the only thing open by default.
  • Terminal (4): docs/APPS.md now says the terminal pull screen shows no looks, light or dark. I put it there rather than the README, since that file already describes looks.
  • dark: prefix (5): untouched, since it's tracked in #3126.

Not done: the terminal still shows no looks, which you accepted.

Approved

Dark pictures are stored in their own slot, served with the same access rule as the light ones, and shown in a collapsed "In the dark" fold. The fold is a native details element, so it works without script and does not stack four tall pictures on a 360px phone. Every other listing (front, studio, OG, runner) still defaults to light. The fold reuses quiet-details, so there is no new CSS and no new accent. The new tests cover the slot, the theme default, the stale-commit guard and a real pixel check. I did not run the real-Chromium pixel test here; the forge check will be its first run. The terminal never showed looks, and docs/APPS.md now says so.

Two things to do next:

  1. Land #3126 to widen the unique key with theme and drop the dark: page encoding, so no reader has to know about the prefix.
  2. Give the dark pair a way to open without a click when the reader's own system is in dark mode. Consider also a dark row in the check summary vitals, so a dark-only layout shift is caught.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs