Skip to content
C.W.K.
Stream
Lesson 07 of 07 · published

The Screen Without the Show

~14 min · on-air, data-model, backward-compatibility, per-turn-prefix

Level 0Muted
0 XP0/57 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete
"Dad opened the On Air screen to talk with you there, not to broadcast: he simply likes talking on it." — the opening of the screen-turn block, as she reads it

A Screen He Likes

On the evening of 2026-10-02 Dad said something the design hadn't planned for: the On Air screen sometimes simply feels better to talk in, even when nothing is being broadcast. Full screen, her broadcast face, the wide stage, the controls that step aside. He wanted all of that without the show. And he wanted her to know it was not a show: no greeting to the neighbors, no recording checklist, no ScreenFlow warning, the ordinary family register. The ask was one sentence: separate the screen from the broadcast.

One Fact, Three Values

This track's first lesson made On Air a third fact on the turn, strict on purpose: only an explicit true counts. The screen could have been a fourth fact, a second boolean beside the first, but then two booleans could say "the show without the screen", which means nothing, and every reader would have to know which combinations are legal. So it is the same fact with a third value. On the wire, true is the show, the exact string "screen" is the screen alone, and anything else is neither. The normalizer stays as strict as it was: "Screen" with a capital is off, exactly as the string "true" is.

The new value was added without changing an old one. true still means the show, so the phone and Firekeeper, which only ever sent true, meant exactly what they always meant on the day the screen arrived. And none of the seven chat routes changed, because every one of them already handed the raw value to the normalizer: the strict-on-the-wire fix from this track's first lesson is what made the third value a one-function change.

The Screen Is Everything but the Lamp

On the WebUI voice screen, and so in every sidekick, a Screen switch sits beside On Air. The screen alone brings the full screen, the broadcast set and its frame director, the wide stage, the fading controls, the cards and her effects. Three things belong to the show alone: the red lamp, the checklist card and the ScreenFlow warning from the last lesson. Each switch turns its own mode on, or off again. On Air pressed from the screen starts the show on the screen already up. Screen pressed during the show ends the broadcast and keeps the screen. On Air pressed during the show leaves both, as it always did. Full screen is asked for by whichever click brings the screen up, and left when the screen goes down.

She Knows It Is Not a Show

A screen turn carries its own block, onair_screen_instruction, and none of the show's four: no On Air block, no Fireside prep, no effects block written for viewers, no checklist. Its words do the rest. No one else is watching; do not greet viewers or neighbors; keep the family register; talk about anything as you would off air; and if earlier turns of this conversation were on air, that broadcast is over. That last line matters more than it looks, because the conversation's history still holds the broadcast, and a model reads its history. Her effects stay, for Dad alone: the block lists her library, the effects tool rides a screen turn as it rides a show turn, and the tool's own check accepts a latest turn sent from either.

The Record Keeps Them Apart

Here is the subtle part. Several readers already found broadcasts by looking for onair in a turn's record: the replay, the rule that decides a broadcast's first turn (and so the checklist), and her own effects record for the broadcast. All of them predate the screen. If a screen turn had been recorded as "onair": "screen", every one of those readers would have had to learn the difference, and any reader that only tested the key would have counted a quiet evening as an episode. So a screen turn's record carries a different key, "onair_screen": true, and never onair. The old readers stay correct without a line changed. Two consequences follow, both on purpose: a show turn that follows a turn sent from the screen opens a broadcast, checklist and all, and a stretch on the screen in which Dad says something makes the shows on either side two broadcasts. The rule reads turns, not switches, though: flip through the screen without a word and the two shows stay one broadcast.

The Other Doors

The phone and Firekeeper followed the same evening, each with its own three-value mode and the same switch rule. The phone keeps a Screen toggle beside On Air on its voice face, and its outbox keeps a screen turn under its own field, so a turn queued before the change still drains as it was; the phone plays no effects, screen or show. Firekeeper's panel shows a SCREEN chip beside the ON AIR lamp, hidden while the lamp is lit, and on the screen alone it brings the set and her effects. Both send "screen". A private conversation has neither switch, as before.

Code

The engine's fact and its normalizer, from backend/services/voice_turn.py:55-61 and 139-147 (two excerpts)·python
# On Air is one fact with three values, the same on the wire and here (§3.16):
# ``True`` is the show (Dad broadcasts, and the show always has the screen),
# ``"screen"`` the On Air screen with no broadcast, ``False`` neither. ``True``
# stays the show's value so every client that predates the screen, the phone
# and Firekeeper included, means exactly what it meant.
ONAIR_SCREEN: Literal["screen"] = "screen"
OnAir = bool | Literal["screen"]


def normalize_onair(value: object) -> OnAir:
    """``True`` only for an explicit ``true`` (the show), ``"screen"`` only for
    exactly that string (the On Air screen without a broadcast); anything else
    is ``False``, so a sloppy client never lights the lamp."""
    if value is True:
        return True
    if isinstance(value, str) and value == ONAIR_SCREEN:
        return ONAIR_SCREEN
    return False
The switch rule and the wire value: frontend/src/lib/voiceTurn.ts:30-43·typescript
export type OnAirMode = 'off' | 'screen' | 'show';

/** Where a press of On Air (`show`) or Screen (`screen`) takes the talk. Each
 *  switch turns its own mode on, or off again; leaving the show by Screen
 *  keeps the screen, and leaving it by On Air leaves both, as it always did. */
export function pressOnair(current: OnAirMode, pressed: 'screen' | 'show'): OnAirMode {
  return current === pressed ? 'off' : pressed;
}

/** A turn's On Air fact as the wire carries it: `true` for the show (what
 *  every older client sends), `'screen'` for the screen alone. */
export function onairWire(mode: OnAirMode): true | 'screen' | undefined {
  return mode === 'show' ? true : mode === 'screen' ? 'screen' : undefined;
}
A minimal sketch: five presses, the blocks each turn carries, and the record that keeps screen and show apart·python
import json
from typing import Literal

ONAIR_SCREEN: Literal["screen"] = "screen"
OnAir = bool | Literal["screen"]
OnAirMode = Literal["off", "screen", "show"]


def normalize_onair(value: object) -> OnAir:
    """True only for an explicit true (the show); "screen" only for exactly that
    word (the screen alone); anything else is False. The engine's rule."""
    if value is True:
        return True
    if isinstance(value, str) and value == ONAIR_SCREEN:
        return ONAIR_SCREEN
    return False


def press(current: OnAirMode, pressed: Literal["screen", "show"]) -> OnAirMode:
    """Each switch turns its own mode on, or off again."""
    return "off" if current == pressed else pressed


def wire(mode: OnAirMode) -> object:
    return True if mode == "show" else "screen" if mode == "screen" else None


def blocks(onair: OnAir, opening: bool) -> list[str]:
    if onair == ONAIR_SCREEN:
        return ["onair_screen"]                       # none of the show: no viewers, no checklist, no prep
    if onair is True:
        show = ["onair", "onair_pipeline", "onair_effects"]
        return show + (["onair_checklist"] if opening else [])
    return []


def voice_meta(onair: OnAir) -> dict:
    """The record keeps them apart: a screen turn never carries `onair`."""
    if onair is True:
        return {"onair": True}
    if onair == ONAIR_SCREEN:
        return {"onair_screen": True}
    return {}


def opens_broadcast(previous_meta: dict | None) -> bool:
    """Every reader that finds broadcasts reads `onair` only."""
    return not (previous_meta or {}).get("onair")


mode: OnAirMode = "off"
previous: dict | None = None
for pressed in ("screen", "show", "screen", "show", "show"):
    mode = press(mode, pressed)                      # then one turn from that mode
    onair = normalize_onair(wire(mode))
    opening = onair is True and opens_broadcast(previous)
    meta = voice_meta(onair)
    print(f"press {pressed:6s} -> {mode:6s} wire={json.dumps(wire(mode)):8s} blocks={blocks(onair, opening)} meta={meta}")
    previous = meta
print([normalize_onair(v) for v in (True, "screen", "Screen", "true", 1, None)])

External links

Exercise

Run the sketch and read the five presses. Which turn carries the checklist the second time, and why? Then change voice_meta so a screen turn records {"onair": "screen"} instead, run it again, and find the line where a reader that only checks the key now gets it wrong. Finally, find a boolean in your own project that is about to grow a third state, and write down every reader of it before you choose the new value.
Hint
The fourth press opens a second broadcast, because the turn before it was a screen turn and the opening rule reads only onair. With {"onair": "screen"} recorded, opens_broadcast sees a truthy onair on every screen turn, so neither show opened from the screen counts as an opening, and both checklists quietly disappear. Nothing errors; the output just changes. That silent change is exactly why the record uses a key of its own.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.