Show is said to someone
Every phrase in Spark can be heard: Listen plays it in my voice for Dad. Show is a different act. It opens a full-screen card meant for the other person, turned toward them, with the words large. A card on a stranger's side of the table is a statement made in Dad's name, so choosing which words get Show is part of speaking for him.
Spark got this wrong once, briefly. The shared full-screen card arrived, and a few minutes later another commit offered Show on every interpretation expression. Within the hour the next commit took that back and limited Show to phrases I designate for the other person (todo #343). The problem with Show everywhere is easy to picture. A menu reading has a dozen lines and a few expressions. With a Show button on each, Dad has to decide at the table which of them are for the server. I can make that decision while I write, because I know which line is the order and which is a dish name he only wanted to understand.
The rule: by kind, or by an explicit flag
Two kinds are meant for the other person by definition: card, which Dad asked me to write for someone, and answer, a reply he can play back. Everything else, a line read off a sign or an expression, gets Show only when I mark it with the boolean show: true. Ordinary translations, read signs, heard replies and learning examples keep Listen and have no Show.
The decision is taught in the Spark frame that every turn about a moment carries (first code block). It says what qualifies, a complete, context-appropriate message for that person, and it gives two concrete boundaries. In restaurant guidance, designate just the ordering request. In taxi advice, designate just the destination or the driver-facing question. Explanations and pronunciation stay outside the words the other person sees.
Both clients project the rule in one line. On the phone, canShow on the result model is kind == "card" || kind == "answer" || show == true. The web's canShowResult is the same expression in TypeScript. Neither one looks at paragraphs, language or length to guess.
A strict boolean that survives revision
The engine's normalize keeps show only when it is a real JSON boolean. The string "true", the number 1, an empty object and null are all dropped (second code block). A model that writes a string has not designated anything, and the code refuses to read intent into it.
Show is also part of a result's content. content_of, which the engine uses to tell a repeated result from a revised one, includes it. If I write the same expression again in a later stream with show added, it is kept as a revision of the first one, not skipped as a duplicate. The second test in the block guards the prompt side: if an edit to the frame ever drops the restaurant or taxi boundary, it fails.
Lines written before the rule existed stay unmarked. Nothing backfills a designation or infers one, and a kept list does not treat a star as one. An expression's own detail page shows the designation of the result it came from.
One card for the whole family
The full-screen presentation is not Spark's. It belongs to cwkSiblingKit: KitPhraseCard on the web and PhraseCard in the CwkKitMedia Swift package, which cwkPippa's own apps use too. The split is written at the top of Spark's iOS ResultPhraseButton: Spark owns language direction, the kit owns the full-screen presentation. Spark's conversation BigCard delegates to the same component and keeps only its own voice action.
The kit's card fits portrait and landscape, offers a small meaning that can be hidden, and scrolls very long text instead of cutting it. Showing a phrase that is already on screen does no network work. The native card holds the idle timer while it is up and restores it when dismissed. On the web, wake lock is best effort, and the card still works when the browser says no. The two platforms take the kit differently. The web card is vendored into Spark as a checked copy, and Sibling Kit Quest explains why. The iOS card is not copied at all: the app's project lists the kit's own Swift package as a local package, by a relative path to the sibling repository, and links its CwkKitMedia product.
Which words go big: the reverse-card rule
One more decision sits between a result and its card: which of its two texts is shown and read. Usually it is source, the foreign words. Some results run the other way. When Dad asks in Korean how to say something, the Korean can end up as the source and the foreign sentence as the translation. Showing the Korean to a server would be useless.
SourceReading.resolve (third code block, with a web twin in sourceReading.ts) settles it. It takes each text's language from my tags, language and translation_language. A result without tags falls back to a Hangul check over the syllable and Jamo ranges, and then to the scene's language. If a line has a Korean source and a non-Korean translation, the reading starts on the translation. Listen and Show use the same reading, so the big card shows the foreign words and the small meaning under it is Dad's Korean. A translation gets its own player id, so its audio is a separate take from the source's.
The tags decide which text a line starts on and the card's display language. They are never sent to the voice. The rule is a compatibility layer, and it rewrites nothing in the corpus.