VEILFALL is finished enough to have a store listing, so this week was asset week: icons, promo tiles, five screenshots, a listing document, four films. I expected it to be packaging work. Instead the capture harness found three real defects, and the reason is worth writing down: every one of them was invisible at the width I had been developing against.
The project page, with the full write-up and the privacy policy, is here: VEILFALL.
Capturing a side panel without cheating
Google’s screenshot slot wants 1280×800, full bleed, square corners, no padding. A side panel is 380 pixels wide and very tall. The obvious move — centre the panel on a dark background until it fills the canvas — is exactly the thing the rule forbids, because that background is padding no matter how nicely it is shaded.
The honest version is to capture the panel in situ: a real Chrome window, the extension loaded unpacked, the panel docked, the whole window grabbed off the X root. Then every pixel in the frame is real — browser chrome, the page behind it, the panel — which is also what an installer actually sees.
Two things made that harder than it sounds. chrome.sidePanel.open() requires a user gesture, and there is no toolbar button to click from a script. The way through is that Playwright dispatches input through the DevTools protocol, which Chrome treats as trusted — so a real click on a button injected into an extension page satisfies the gesture requirement with no flag and no mock. The second problem is that launchPersistentContext never attaches the docked panel; pages() only ever lists tabs. The panel is a page target, it just needs a second connection over the debugging port to see it.
With those two out of the way the harness drives a real game through the real engine and grabs the window at named beats. Nothing composed, nothing retouched.
The panel is not 380 pixels wide
Chrome’s side-panel splitter is draggable. I knew that and had never once tested it, because the development harness pins a 380-pixel viewport and every screenshot I had ever taken was 380 pixels wide.
The first in-situ capture showed the Circle — the ring of nine seats that the whole interface is built around — stretched into a wide ellipse. The cause is one line: the ring is aspect-ratio: 1 with a max-height, and past about 354 pixels the height cap wins while the width keeps growing. At the design width the distortion is a couple of pixels and invisible. At 730 it is an oval. Body prose had the same problem in a quieter way: it ran to a measure nobody would want to read.
The fix is small — cap the ring’s width, and cap the content column past 520 pixels — but I would not have found it, because I had no reason to look. The lesson is not “test more widths”. It is that a capture harness pointed at the real product in its real container is a different instrument from a test suite pointed at the code, and it sees a different class of defect.
Two opponents, one sentence
The discourse screenshot showed six lines of table talk. Two of them, from two different souls, were the same sentence word for word: “We can afford one bad Sealing. Not two, not at 9.”
The offline tier draws lines from small template banks — three options per intent — and each agent drew independently. With nine souls in a room, collisions are not unlikely, they are expected. And a word-for-word echo does not read as two people agreeing. It reads as a bug, which is worse than the bland line it was trying to avoid.
The fix is a set of what has already been said today, passed into the renderer, which redraws on a collision and — if the bank for that intent is exhausted — returns nothing at all. A soul that says nothing this round is indistinguishable from a soul that simply chose not to speak. An echo is not indistinguishable from anything.
Measured before: 8 days out of 91 contained a repeated sentence. After: 0 out of 95, with a regression test that drives the controller and collects the lines as they are produced. The first draft of that test probed the game log instead, found nothing, and would have passed vacuously — it only failed honestly because it also asserted it had seen some days to check.
The limit only the dashboard enforces
The third one never reached a screenshot. manifest.description has a 132-character ceiling that no linter checks, no local tool checks, and nothing surfaces until the Developer Dashboard rejects the package at upload. Mine was 143 characters, because I had written it as marketing copy and the listing summary field is a different, longer field.
That is a wasted round-trip that costs five lines to prevent, so the pre-ship gate now asserts the manifest field limits alongside everything else: description, name, version format, and that all four toolbar icon sizes exist. An over-tight local limit costs nothing. An over-loose one costs an upload.
Where it stands
0.8.0 is built and gated: 155 tests green across four suites, a real-browser end-to-end pass with zero console errors, twelve pre-ship checks, and the package verified from its extracted bytes rather than from the build log. The store listing is written and the assets are cut. Nothing is submitted yet, and the page says so rather than implying otherwise.
Links to the launch video and the Chrome Web Store listing will be added here once each is live.
Full write-up, the measured behaviour table, and the privacy policy: dhseadev.online/projects/veilfall.
