---
name: Contact Sheet
description: Use when the operator has to LOOK at generated images and choose — keeping some, rejecting others, asking for variations on the ones they liked. Ships the Contact Sheet app, which shows everything in files/ as a wall of tiles and writes their decisions to files/picks.json for you to read next turn.
tagline: See all your generated images side by side, keep the ones you like, and ask for more like them.
version: 1
situations: review the images, which one do you like, pick the best image, keep or reject these, show me the options, more like that one, compare these two, contact sheet, approve the stills, choose a cover, image picks
requires-secrets:
requires-packages:
---

# Contact Sheet

## Why a screen

You generate four covers; the operator has to look at four pictures and say which ones survive —
and a chat round-trip turns that into "the second one, no, the other second one". The app draws
every image in `files/` as tiles: they keep, reject, compare two side by side, and press "more like
this" on the one that worked, all without typing a sentence.

What comes back to you is one file, `files/picks.json`, written the moment they decide. You read it
on your next turn, generate variations of what they kept, and leave everything they rejected exactly
where it is.

## The files

- **`files/`** — where `image_generate` saves everything. The app reads that folder and nothing else.
- **`files/picks.json`** — the decisions. The app writes it; you read it.
- **`files/<name>.json`** — the sidecar for one image (see below). The engine writes it; the app
  reads it. Nothing is asked of you.

### `files/picks.json`

```json
{
  "version": 1,
  "picks": {
    "files/image-1756900101000.png": { "decision": "keep",   "at": "2026-09-03T09:14:02.101Z" },
    "files/image-1756900102000.png": { "decision": "reject", "at": "2026-09-03T09:14:06.884Z" },
    "files/image-1756900104000.png": {
      "decision": "reject",
      "note": "hands are wrong",
      "at": "2026-09-03T09:14:11.020Z"
    }
  }
}
```

- Keys are **workspace-relative paths**, always under `files/`. An image with no key has not been
  decided on yet — that is different from a rejection, and you must not treat it as one.
- `decision` is exactly `"keep"` or `"reject"`. `at` is ISO-8601, when the person pressed it.
- `note` is optional and free text. The app never invents one, and it carries yours across unchanged
  when the person changes their mind about that image — so a note you wrote is still there next turn.
- The file is rewritten whole on every decision. Do not hand-merge it while the app is open; write
  what you want the person to see, or ask them to press the buttons.

### The sidecar — `files/image-1756900101000.json`

**The engine writes it. You do not.** Every file `image_generate` saves — and every clip, song, sound
effect and speech file the other media tools save — lands with a `.json` beside it carrying the ask
that produced it. Nothing is required of you: generate, and the sidecars are already there.

```json
{
  "version": 1,
  "kind": "image",
  "tool": "image_generate",
  "prompt": "Wide launch-post cover: low sun over a dark ridge, warm coral sky, space at the top for a headline.",
  "model": "flux-dev",
  "params": { "aspect": "3:2", "seed": 41211, "steps": 28 },
  "createdAt": "2026-09-03T09:13:58.004Z"
}
```

An edit or a composition also carries `source`: the workspace-relative files it was built from.

The app reads `prompt` and nothing else from it, and accepts either spelling of the name
(`image-123.json` beside `image-123.png`, or `image-123.png.json`) — the engine writes the first, and
the second is still read for anything written by hand before this existed. With a sidecar, "more like
this" prefills the composer with the ask that produced that picture; without one it prefills the file
name, which is worth much less to both of you.

An image that arrived some other way — dropped onto the sheet, copied in from elsewhere — has no
sidecar, and that is fine: the app falls back to its file name. Writing one for such a file by hand
is allowed and never necessary. **Never rewrite a sidecar the engine wrote**: it is the record of
what was actually asked for, and an "improved" prompt in it is a lie about a picture you cannot
regenerate.

## The loop

1. You generate images into `files/`. The engine drops a `.json` sidecar beside each one; you
   write nothing.
2. You tell them to open **Contact Sheet** — "open the Contact Sheet app and keep the ones that
   work; I'll read your picks."
3. They keep, reject and compare. Every press rewrites `files/picks.json`.
4. **Next turn, read `files/picks.json` first.** Then:
   - **kept** → these are the brief. Generate variations of them, reuse them as reference images
     (`imagePaths`), or ship them.
   - **rejected** → do not regenerate the same thing. Read the `note` when there is one; it says
     what was wrong. If several rejections share a fault, say so out loud before spending on more.
   - **undecided** → not yet looked at. Ask, or wait; never count it as either answer.
5. The app's buttons prefill the composer and never send. When the person presses "more like this"
   or "Ask for more like the kept ones", what arrives is a message from THEM — read it as such.

## What you must never do

- **Never delete a rejected image.** Not to tidy up, not to save space, not because they said
  "no" to it. A rejection is an opinion about one round; the person decides what happens to the
  file, and they often come back to one. The app itself has no delete either — that is deliberate.
- Never write `files/picks.json` to fake a decision. It is the person's half of the conversation.
- Never move or rename an image once it is on the sheet: the picks are keyed by path, and a rename
  silently orphans a decision.
- Never assume an undecided image is rejected.

## The app

`ui/ui.json` declares one app, `contact-sheet`, attached to `files/`. It asks for:

| capability | what it does with it |
|---|---|
| `files` read/write `files/**` | list the folder, read image bytes and sidecars, write `files/picks.json` |
| `media` | show the pictures (bytes arrive over the bridge and become Blob URLs) |
| `state` | remember one setting — "hide rejected" — between opens |
| `import` into `files` | save images the person drops onto the sheet |

It has no network, runs no tools, and paginates at 200 tiles. A New Session action, **"Review
generated images"**, starts the whole loop from the New Session dropdown.

- `ui/app/index.html` — the page (`app.js`, `app.css`, `00.css` beside it)
- `ui/samples/files/**` — what the app shows when the folder is empty, and what the store's
  screenshots are taken against
- `apps/00d/test/contact-sheet-app.test.ts` — the loop, walked against the kit harness

## Notes for me

Write here what you learn about this operator's taste — which prompts they keep, which they reject,
the faults they call out — so the next batch starts closer to what they want.
