Six surfaces on paper, four pages in the build
The architecture document lists the studio's main surfaces as six: Souls, Sets, On Air, Studio, Batches and References. Open the app and you find four pages in the sidebar. The code is the authority here, and the list lives in App.tsx: Soul collections, Create image, Image library, and Batches & review.
The other names did not vanish. They moved inside the four.
| Name in the document | Where it lives in the shipped app |
|---|---|
| Souls, Sets | Soul collections: one soul's everyday and preset sets |
| On Air | A channel toggle inside Soul collections, beside Everyday & presets |
| References | A soul's reference editor, opened from that soul, plus the ordered reference tray |
| Studio | A segmented control inside Create image: studio, diary, thumbnail, clip, create, regenerate, remix |
| Batches | Batches & review |
One English interface, dark by default, serves a desk monitor, an iPad and a phone. The same built bundle goes into the iOS app, which the next lesson opens up.
Hidden, not unmounted
Three of the pages are rendered all the time and switched with the hidden attribute. Leaving Create image for the library does not throw away the prompt you were halfway through, the subjects you picked or the generators you set up, because the component never unmounted. Batches & review is the exception: it mounts only while it is open. That page polls the engine every 2.5 seconds while any task is queued, preparing, submitting, running or copying, and a hidden page that keeps polling is a quiet cost. The rule underneath is simple. Keep a page alive when it holds the user's unfinished work. Let it go when it only holds a view of the server.
Drafts that survive a restart
Hidden pages survive a page switch. A relaunch needs storage. draft.ts is 18 lines: read and save JSON under an avatars: prefix in localStorage, and swallow every storage error. Its comment states the boundary: device-local drafts complement server-owned jobs and selections, and a quota or privacy setting must never prevent editing or submission.
Three kinds of draft use it. The ordered reference tray is saved as a list of asset ids and reloaded from the engine on start, skipping any that no longer resolve. Each generation form is saved under a key per set, or scene when no set is chosen. And the review step is saved too, which matters more than it looks. The review holds the frozen request, including the request_id minted with crypto.randomUUID() when the review opened. Close the studio on the review screen and the review stays saved under its form's key. A review made without a set is waiting the next time Create image opens. A set's review comes back when that set is opened for generation again, because which set you came from is not saved and a relaunch starts on Soul collections. Either way, pressing submit sends the same id, so the engine answers with the batch it may already have created. The Record Keeper track tells the server side of that promise.
None of this is canonical work. Batches, selections and publications live on the engine, shared by web and phone. A draft is only the unfinished sentence on one device.
The library and the lightbox
Image library searches labels and prompts, filters by soul and by origin, shows archived images on request, and pages 24 at a time with first, last and a page number you can type. A query waits 180 milliseconds after the last keystroke, and a page past the end jumps to the last real page.
Library, collections and batch review all open the same lightbox, built on a native <dialog> opened with showModal(). Escape closes it, the arrow keys move through the gallery, and closing returns focus to the element that opened it. Zoom runs from 1x to 4x in half steps, with a Fit button. A clip plays inline in a <video> with playsInline and a poster frame, and Open in Ashen Reel asks the engine to open the file it holds on the Mac that runs Avatars; the browser names only the asset. The architecture's rule for the viewer is that opening or navigating it never changes a candidate selection or publishes a frame.
Small things a phone notices
- Safe areas. The page uses
viewport-fit=cover, so every top or bottom surface pads itself withenv(safe-area-inset-*). - Buttons stay on one line. A shared kit stylesheet keeps a control's label from wrapping; prose buttons opt out by name.
- A failure that names its cause. A fetch that reached nothing says to check Tailscale, unless the page was talking to its own Mac's loopback, where Tailscale is not the problem.
- One sync event. A
studio:syncevent reloads souls, sets, generators and config, and the open batch listens too. The iOS host fires it after every successful connection check.