Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,13 +307,13 @@ installation, players, backend services, the site, or production deployment.
- `media upload --tag TAG` stores a 1–32 letter-or-digit tag on the ready
object at declare. It is not redeclared at commit. Pattern
`^[A-Za-z0-9]{1,32}$`; reject other values locally as `usage_error`.
- `media list [--tag TAG] [--kind image|video]` forwards those query
filters to `GET /api/v1/media`. Untagged objects are omitted when `--tag`
- `media list [--tag TAG] [--primitive image|video]` forwards `tag` and
`primitive` parameters to `GET /api/v1/media`. Untagged objects are omitted when `--tag`
is present.
- `media update <id> (--tag TAG | --clear-tag) --if-match REVISION` binds
`PATCH /api/v1/media/{id}`. Exactly one of `--tag` or `--clear-tag`.
`--clear-tag` sends `null`. There is no other media metadata patch. Do
not add an update path for filename, kind, or codecs.
not add an update path for filename, primitive, or codecs.
- This surface is **repository-ready** on public `main`. It is not in the
locked plugin bundle. Do not claim marketplace or deployed.

Expand Down Expand Up @@ -342,10 +342,10 @@ installation, players, backend services, the site, or production deployment.
## Page motion

- Default templated pages stay `{ type: "crossfade", duration_ms: 200 }` with
no placement `enter`. Do not flip that default to swipe.
no object `enter`. Do not flip that default to swipe.
- Full pages are opaque: the CLI forwards `transition.type` swipe variants
(`swipe-left`, `swipe-right`, `swipe-up`, `swipe-down`) and optional
placement `enter: { type }` with that same object name. No snake_case
object `enter: { type }` with that same object name. No snake_case
inside `enter`. `duration_ms` is required, 0 through 60000. Swipe
authoring default when chosen is `duration_ms: 600`.
- Teach swipe and `enter` as spare emphasis, not as the ordinary page.
Expand All @@ -354,6 +354,7 @@ installation, players, backend services, the site, or production deployment.
- `Application` carries no `state` and no `release_id`. It reports
`latest_ready_release`. `OperationAccepted.release_id` is required, so
`app upload` reports the release id without waiting on the operation result.
An application primitive pins that `release_id`; it has no selector.
Knowing the id is not readiness; the operation still decides that.
Optional `--name` (at most 120 characters, no line break) is sent as
`ScreenRig-Application-Name`. Every upload still creates a new
Expand Down Expand Up @@ -407,10 +408,13 @@ installation, players, backend services, the site, or production deployment.
redacts and bounds the stderr tail; keep it on that path.
- Transcoded bytes live in a `mkdtemp` directory the caller removes. Every
failure path after that directory exists must remove it.
- Playlist pages the CLI emits may use only `image`, `video`, `iframe`, and
`application`. Do not emit native `text`, `box`, or `line`. Copy and chrome
- Playlist pages the CLI emits use `primitives[]`. Four wire primitives exist:
`image`, `video`, `iframe`, and `application`, named by the `primitive`
field. Image and video require a `selector` whose `by` is `id`, `ids`, `all`,
or `tag`; iframe and application do not take selectors. Do not emit native
`text`, `box`, or `line`. Copy and chrome
are composed locally with `compose catalog` / `compose render`, uploaded as
`image`, then placed as one image. `playlist templates` is a local catalog.
`image`, then used as one image primitive. `playlist templates` is a local catalog.
Templated pages that would emit vector chrome fail with `usage_error`
pointing at those compose commands. Do not silently rasterize and upload.
- `canvas.background` is a solid uppercase `#RRGGBBAA` or a top-to-bottom
Expand Down
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,41 +164,41 @@ ScreenRig content has three families: static images, including stills produced
by local compose; motion video; and web content delivered as an `iframe` or
`application`.

Playlist pages on the wire use `image`, `video`, `iframe`, and `application`
only. Copy and chrome are composed locally, uploaded as `image`, then placed
as one image. `playlist templates` is a local catalog of slide ids; templates
that would emit native `text`, `box`, or `line` fail with a pointer at
Four wire primitives are supported: `image`, `video`, `iframe`, and
`application`. Copy and chrome are composed locally, uploaded as `image`,
then used as one image primitive. `playlist templates` is a local catalog of
slide ids; templates that would emit native `text`, `box`, or `line` fail with a pointer at
`compose catalog` and `compose render`. Picture-only templates still expand
to image or video placements with selectors. A page without `template` is
forwarded unchanged when its placements are those wire kinds.
to image or video primitives with selectors. A page without `template` is
forwarded unchanged when its `primitives` use those four wire primitives.
`canvas.background` is a solid uppercase `#RRGGBBAA` or a top-to-bottom
linear gradient (`type` `linear`, 2 through 8 strictly increasing stops,
first `at` 0, last `at` 1, no angle). Image and video placements write a
first `at` 0, last `at` 1, no angle). Image and video primitives write a
`selector` (`by` is `id`, `ids`, `all`, or `tag`). Do not put `media_id` on
the content object, and do not send server-resolved `items`. Page advance
uses `duration`, `application`, or `media_end`. There is no `video_end`
mode.
the primitive itself; it belongs inside the selector. Iframe and application
primitives do not take a selector. Page advance uses `duration`, `application`,
or `media_end`. There is no `video_end` mode.

Default pages use `{ "type": "crossfade", "duration_ms": 200 }` and put no
`enter` on placements. `transition.type` may also be `swipe-left`,
`enter` on objects. `transition.type` may also be `swipe-left`,
`swipe-right`, `swipe-up`, or `swipe-down`. `duration_ms` is required and
runs from 0 through 60000. When a swipe type is chosen, write
`duration_ms: 600`. That is the authoring default for swipe, not a schema
default, and it does not change the template default. Swipe is the incoming
page's type. The outgoing page follows so the edges stay touching. The name
is motion direction: `swipe-left` moves content left.

Optional placement `enter` is `{ "type": "..." }` with that same object
name on playlist JSON. There is no snake_case rename inside it. Types are
Optional object `enter` is `{ "type": "..." }` with that same object name on
playlist JSON. There is no snake_case rename inside it. Types are
`fade-up`, `fade-down`, `fade-left`, `fade-right`, `fade-in`, `zoom-in`,
and `zoom-out`. Absent means no object animation. Use swipe and `enter`
sparingly, for emphasis or a particular style, not on every page. If you
want object animation, layer the content and put the motion on the
top-layer text or images. Do not animate every placement. Object enter
want object animation, layer the primitives and put the motion on the
top-layer text or images. Do not animate every object. Object enter
starts invisible. It runs 500 ms after the page occupies the full
viewport, for 400 ms. Those delays are contract constants, not author
fields and not CLI flags. The pinned CLI implements these playlist document
fields, and the control plane accepts swipe types and placement `enter`. This
fields, and the control plane accepts swipe types and object `enter`. This
does not establish marketplace availability or deployment.

The ordinary pair command currently accepts six canonical characters:
Expand Down Expand Up @@ -405,7 +405,7 @@ renames it into place. The bundle contains `screenrig-bundle.json`,
`playlist.json`, and content-addressed `media/<sha256>.<canonical-ext>` files.
The manifest records `selector_policy: "snapshot"` and
`comments_policy: "excluded"`. Dynamic `all` and `tag` selectors become exact
`id` or `ids` snapshots. Iframes remain URLs. Application placements are
`id` or `ids` snapshots. Iframes remain URLs. Application primitives are
rejected before any media download because v1 has no application-package
export.

Expand Down Expand Up @@ -509,7 +509,7 @@ relative to the spec file. The CLI does not fetch URLs.
`--open` is only for viewing the still on this computer. Agent vision reads
the file path. Do not cat pixels into chat.

A playlist page for that still is one `image` placement whose `rect` is the
A playlist page for that still is one `image` primitive whose `rect` is the
canvas and whose `content_fit` is `fill`.

## Events
Expand All @@ -530,7 +530,7 @@ screenrig --json playback list --screen-id scr_01 --day 2026-08-14
```

A human line looks like
`at=2026-08-14T17:00:00.000Z type=application.event severity=info code=cta.pressed placement_id=weather`.
`at=2026-08-14T17:00:00.000Z type=application.event severity=info code=cta.pressed primitive_id=weather`.
It carries `at`, `type`, `severity`, optional resource fields, scalar
details, and a non-canned `message`. Canned server sentences are omitted. An
`application.event` or `runtime.reported` with no remaining data is silent. A
Expand Down Expand Up @@ -664,12 +664,13 @@ screenrig --json media upload ./clip.mov
screenrig --json media upload ./clip.mov --codec hevc
screenrig --json media upload ./poster.png --no-transcode
screenrig --json media upload ./lobby-welcome.png --tag lobby
screenrig --json media list --tag lobby --kind image
screenrig --json media list --tag lobby --primitive image
screenrig --json media update med_01 --tag lobby --if-match 1
screenrig --json media update med_01 --clear-tag --if-match 2
```

`media list` forwards `--tag` and `--kind image|video` as query filters.
`media list` forwards `--tag` and `--primitive image|video` to
`GET /api/v1/media` as `tag` and `primitive` parameters.
Untagged objects are omitted when `--tag` is present. `media update` patches
the tag only; `--clear-tag` sends `null`. There is no other media metadata
patch.
Expand Down
6 changes: 3 additions & 3 deletions src/adapters/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ test("adapter shapes mirror generated OpenAPI contract fields and Capabilities l
"updated_at",
]);
// release_id is required, not optional. The server has always returned it and
// it is the only handle a playlist placement accepts, so a caller must never
// it is the only handle an application primitive accepts, so a caller must never
// have to poll the operation to learn it.
const operationAccepted = interfaceBody(source, "OperationAccepted");
assert.deepEqual(quotedProperties(operationAccepted), [
Expand Down Expand Up @@ -501,7 +501,7 @@ test("published problem codes include payment_required at 402", () => {
assert.doesNotMatch(source, /stripe|x402/i);
});

test("playlist transition types include swipe and optional placement enter", () => {
test("pinned backend snapshot includes swipe and its legacy placement enter schema", () => {
const generated = readFileSync(GENERATED_CONTRACT, "utf8");
const openapi = readFileSync(OPENAPI_CONTRACT, "utf8");

Expand Down Expand Up @@ -570,7 +570,7 @@ test("playlist writes send a media selector and media_end, not a singular media_
assert.doesNotMatch(openapi, /enum: \[video_end\]/);
});

test("playlist and runtime placements are image, video, iframe, and application only", () => {
test("pinned backend snapshot still carries the pre-primitive placement unions", () => {
const generated = readFileSync(GENERATED_CONTRACT, "utf8");
const openapi = readFileSync(OPENAPI_CONTRACT, "utf8");

Expand Down
6 changes: 3 additions & 3 deletions src/adapters/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export interface OperationAccepted {
id: string;
/**
* Release created by this upload. The server has always returned it and the
* contract now requires it. It is the only handle a playlist placement
* contract now requires it. It is the only handle an application primitive
* accepts, so report it rather than making the caller poll the operation.
*/
release_id: string;
Expand Down Expand Up @@ -238,11 +238,11 @@ export function limitsFromCapabilities(capabilities: Capabilities): ArchiveLimit
/**
* Full playlist pages stay opaque: the CLI forwards author-supplied playlist
* JSON unchanged, including `transition.type` swipe variants and optional
* placement `enter`. Templated pages are the exception — the CLI expands
* object `enter`. Templated pages are the exception — the CLI expands
* `template` + `slots` into an ordinary write page in `playlist-templates.ts`
* and never sends `template` to the server. Omitted templated transitions stay
* `{ type: "crossfade", duration_ms: 200 }` with no `enter`. The contract's
* page, placement, and content schemas are still not mirrored here except for
* page and primitive schemas are still not mirrored here except for
* the fields that expander writes. Mirror a schema only when the CLI builds or
* reads its fields.
*
Expand Down
1 change: 1 addition & 0 deletions src/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const VALUE_FLAGS = new Set([
"body-file",
"command",
"kind",
"primitive",
"level",
"text",
"duration-ms",
Expand Down
Loading
Loading