@roku-sdk/ai 0.0.0-stage → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/package.json +47 -3
  4. package/skills/ads-and-raf/SKILL.md +231 -0
  5. package/skills/ads-and-raf/references/integration-flows.md +332 -0
  6. package/skills/app-packaging/SKILL.md +107 -0
  7. package/skills/app-performance/SKILL.md +133 -0
  8. package/skills/cli-dev-loop/SKILL.md +217 -0
  9. package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
  10. package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
  11. package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
  12. package/skills/data-entry-keyboards/SKILL.md +160 -0
  13. package/skills/device-management/SKILL.md +158 -0
  14. package/skills/dialogs-and-modals/SKILL.md +210 -0
  15. package/skills/effects/SKILL.md +183 -0
  16. package/skills/fetch-and-data-loading/SKILL.md +164 -0
  17. package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
  18. package/skills/flex-grid/SKILL.md +236 -0
  19. package/skills/flex-grid/references/focus-integration.md +355 -0
  20. package/skills/flex-grid/references/plugins.md +201 -0
  21. package/skills/focus-management/SKILL.md +210 -0
  22. package/skills/focus-management/references/focus-recipes.md +290 -0
  23. package/skills/fonts-and-glyphs/SKILL.md +231 -0
  24. package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
  25. package/skills/logging-and-errors/SKILL.md +227 -0
  26. package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
  27. package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
  28. package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
  29. package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
  30. package/skills/mcp-setup/SKILL.md +131 -0
  31. package/skills/project-setup/SKILL.md +210 -0
  32. package/skills/project-setup/references/roku-config.md +120 -0
  33. package/skills/screen-navigation/SKILL.md +166 -0
  34. package/skills/sdk-components/SKILL.md +213 -0
  35. package/skills/sdk-components/references/dialogs.md +148 -0
  36. package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
  37. package/skills/sdk-installation/SKILL.md +129 -0
  38. package/skills/slideshow/SKILL.md +157 -0
  39. package/skills/slideshow/references/focus-and-handoff.md +187 -0
  40. package/skills/solid-on-roku/SKILL.md +181 -0
  41. package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
  42. package/skills/testing/SKILL.md +147 -0
  43. package/skills/testing/references/async-and-timing.md +97 -0
  44. package/skills/user-accounts-and-purchases/SKILL.md +164 -0
  45. package/skills-manifest.json +316 -0
  46. package/skills-manifest.schema.json +62 -0
  47. package/src/__tests__/manifest.test.ts +267 -0
  48. package/src/__tests__/snippets.test.ts +221 -0
  49. package/src/__tests__/trigger-routing.test.ts +77 -0
  50. package/src/index.ts +36 -0
  51. package/src/manifest.ts +215 -0
  52. package/src/router.ts +147 -0
  53. package/src/snippets.ts +212 -0
@@ -0,0 +1,231 @@
1
+ # Fonts and Glyphs
2
+
3
+ ## Overview
4
+
5
+ Two questions decide whether text in an RSG SDK app renders the way you meant: which typeface is
6
+ supplying the characters, and whether that typeface actually has them. Get the first wrong and
7
+ text comes out at a size nobody can read, or ignores the size you set. Get the second wrong and
8
+ you ship empty boxes — and the boxes appear on a device you did not test, because the firmware's
9
+ fallback font hides the problem everywhere else.
10
+
11
+ ## When to Use
12
+
13
+ - text rendering as empty boxes, on any device or only on some, and whether `fallbackGlyph` helps
14
+ - symbols that were fine until the app started shipping a typeface of its own
15
+ - a symbol needed in a label — arrow, chevron, star, check mark, remote-button icon
16
+ - text that renders far too small, or ignores the size you set
17
+ - shipping a `.ttf` or `.otf` with the app, and a build that refuses two of them
18
+ - deciding whether a shape should be a character at all
19
+
20
+ ### When not to use
21
+
22
+ | Situation | Use instead | Because |
23
+ |---|---|---|
24
+ | Which node draws the text — `Label`, `SimpleLabel`, `ScrollingLabel`, `MultiStyleLabel` | The `sdk-components` skill | This skill owns the typeface, that one owns the node |
25
+ | Brand marks, multi-colour or gradient artwork | The `sdk-components` skill | That is a `Poster` and an image, not a character |
26
+ | Where an asset file belongs in the project before anything can import it | The `project-setup` skill | Project layout is that skill's job |
27
+
28
+ ## Read the reference first for this task
29
+
30
+ | If the request is | Read now, before answering |
31
+ |---|---|
32
+ | Naming a specific glyph constant, or auditing which ones survive a custom font | `references/glyph-tables.md` |
33
+
34
+ The body tells you which *group* a symbol falls in and what that means. The 45 constants, their
35
+ codepoints and their three groups are only in the reference. Guessing a constant name is the one
36
+ mistake here that compiles.
37
+
38
+ ## Quick Reference
39
+
40
+ | Job | Reach for |
41
+ |---|---|
42
+ | Any arrow, chevron, star, tick, or remote-button icon | A named constant from `@roku-sdk/components/glyphs` — never a typed or escaped literal |
43
+ | A character you can simply type (`·`, `°`, currency, quotes, maths) | Type it. These get no constants on purpose |
44
+ | A brand mark or anything multi-colour | Not a character at all — that is `sdk-components` territory |
45
+ | Set a typeface on a `Label` | A `Font` node in the `font` prop, with **both** `uri` and `size` |
46
+ | `Font.size` | Points. **Defaults to 1**, which is why an otherwise correct font renders as a line of dots |
47
+ | Set a typeface on a `SimpleLabel` | `fontUri` plus `fontSize` — no `Font` node |
48
+ | Built-in font names | Ten. The eight `font:…SystemFont` names are **fixed size and ignore `fontSize`**; only `font:SystemFontFile` and `font:BoldSystemFontFile` respect it |
49
+ | Ship a typeface with the app | `import fontUri from "./fonts/whatever.ttf"` — the import resolves to the packaged path at build time |
50
+ | Two font files with the same filename | A build error if their contents differ, reuse if identical. Rename one |
51
+ | Wait until a font can be measured | `whenFontReady(ref)` or `createFontReady(fontRef)` from `@roku-sdk/rsg-ts/runtime` — never a fixed timer |
52
+ | Text arriving from outside with characters your font lacks | Constrain the text or widen the typeface. **`Font.fallbackGlyph` does not substitute anything on current firmware** — see instruction 8 |
53
+ | A `Font` with a `size` and no `uri` | Draws nothing at all. It does not fall back to the system font — omit the `Font` node instead |
54
+ | Which symbols survive a custom app font | The 32 private-use glyphs and `●` `★` `☆`. The other ten do not |
55
+
56
+ ## Instructions
57
+
58
+ 1. **Take symbols from the constants, not from your keyboard or an escape.** Import by name from
59
+ `@roku-sdk/components/glyphs` and interpolate. A literal like `"\uE025"` is the same character
60
+ today and an unlabelled magic number the moment the font revision moves; the constant for that
61
+ one is `LEFT_BUTTON_GLYPH`.
62
+
63
+ 2. **Know the one fact that decides whether a symbol renders: which font supplies it.** The
64
+ constants are grouped by that, not by codepoint:
65
+ - **RokuFont01 private-use glyphs** (32 of them) — RokuFont01 is the firmware's system-wide
66
+ fallback font, so these render under a system font *and* under a custom app font.
67
+ - **Unicode characters that RokuFont01 backs** — `●` `★` `☆`. Same fallback, so also safe.
68
+ - **RokuUI-Regular symbols** (10 of them — the arrows, dashes, bullet, ellipsis, tick and
69
+ ballot X). RokuUI-Regular is the default UI font but is **not** in the fallback chain. Under
70
+ a system font they are fine; under a custom app font they get no backstop and go tofu unless
71
+ your font happens to supply them.
72
+
73
+ So the counter-intuitive part: if your app ships a typeface, the constants that need care are
74
+ the plain-Unicode-looking ones, not the exotic private-use ones.
75
+
76
+ 3. **Remember that a custom font wins outright where it defines a codepoint.** No fallback occurs.
77
+ A licensed font carrying its own private-use glyphs at `U+E0xx` will render *those* in place of
78
+ the Roku icons, which looks like corruption rather than a font choice.
79
+
80
+ 4. **Give a `Font` node a `size`, always.** `Font.size` is in points and defaults to **1**. A
81
+ `Font` with a correct `uri` and no `size` is the classic "the font loaded but the text is
82
+ unreadable" report. Set both together and treat them as one prop.
83
+
84
+ 5. **On `SimpleLabel`, use `fontUri` and `fontSize` — and know which built-in names ignore the
85
+ size.** Of the ten built-in names, eight are fixed-size (`font:Smallest…`, `font:Small…`,
86
+ `font:Medium…`, `font:Large…`, each plain and `Bold`) and **discard `fontSize` silently**. Only
87
+ `font:SystemFontFile` and `font:BoldSystemFontFile`, and any font file of your own, respect it.
88
+ If a size change does nothing, the name is the reason.
89
+
90
+ 6. **Import font files; do not write packaged paths by hand.** `import fontUri from
91
+ "./fonts/inter.ttf"` gives you a string that is the packaged path, and the build copies the
92
+ file for you. Both `.ttf` and `.otf` are declared. Writing the path as a literal skips the copy
93
+ and ships a reference to a file that is not there.
94
+
95
+ 7. **Expect a build error when two font files share a filename.** The build keys them by filename
96
+ alone, so two files called `Inter.ttf` in different directories collide. Identical contents are
97
+ silently reused; different contents fail the build with a `Font naming conflict` naming both
98
+ sources. Rename one — the directory does not disambiguate them.
99
+
100
+ 8. **Do not plan around `fallbackGlyph`.** `Font.fallbackGlyph` reads like the answer to
101
+ uncontrollable text — a character of your choosing instead of a box — and on current firmware
102
+ it does not deliver one. Measured on a Roku Native running 16.0.4: set as a bare `u25CF` (the
103
+ form the SDK's own documentation shows) and as a literal `●`, against both a symbol only the
104
+ default UI font has and a codepoint no font on the device has, the boxes were unchanged in
105
+ every combination. The property is real and the SDK does forward it; what is missing is any
106
+ visible effect. Treat unsupported characters as something to prevent, not something to
107
+ substitute: constrain the text, or ship a typeface that covers it.
108
+
109
+ 9. **Do not use firmware placeholder syntax for app text.** Translated firmware strings may
110
+ contain tokens like `{BACK_BUTTON_GLYPH}`. Those are runtime text tokens resolved by the
111
+ firmware's own string layer, not TypeScript symbols; putting one in a label renders the braces
112
+ literally. Interpolate the imported constant instead.
113
+
114
+ 10. **Wait for a font with `whenFontReady`, not a timer.** `getFontMeasure()` throws
115
+ `"Font is not yet loaded"` until the GfxFont is published. Current firmware has no Font
116
+ `loadStatus` field; use `whenFontReady(ref)` (promise) or `createFontReady(fontRef)`
117
+ (Solid accessor that cancels on cleanup; pass `() => props.uri` or similar as its second
118
+ argument when the `<font>` node's `uri` or `size` can change) from `@roku-sdk/rsg-ts/runtime`, which keep
119
+ working when one ships. Do not copy the fixed 100ms / 500ms timer stubs from older
120
+ examples — those race on a cold cache. A `<font>` that is not on a drawn label is only
121
+ published if `preUpdate` is set before `uri` and `size`; without it the font never loads
122
+ and the wait never resolves.
123
+
124
+ ## Measuring a font
125
+
126
+ `whenFontReady` and `createFontReady` ship in SDK 2.19.0. On 2.18.0 and earlier they are
127
+ not exported from `@roku-sdk/rsg-ts/runtime`.
128
+
129
+ Capture the Font node's `TmNodeRef` from `ref={node => … node.r}`, then wait before measuring:
130
+
131
+ ```tsx rsg-skill=complete
132
+ import { getFontMeasure, whenFontReady, createFontReady } from "@roku-sdk/rsg-ts/runtime";
133
+ import { createEffect, createSignal, type Component } from "@roku-sdk/solid";
134
+ import type { TmNodeRef } from "@roku-sdk/interfaces";
135
+
136
+ export const MeasuredTitle: Component<{ text: string; uri: string }> = props => {
137
+ const [fontRef, setFontRef] = createSignal<TmNodeRef>();
138
+ // The key restarts the wait when `uri` changes on this same <font> node.
139
+ const fontReady = createFontReady(fontRef, () => props.uri);
140
+ const [width, setWidth] = createSignal(0);
141
+
142
+ createEffect(() => {
143
+ if (!fontReady()) return;
144
+ const ref = fontRef();
145
+ if (!ref) return;
146
+ setWidth(getFontMeasure().getOneLineWidth(ref, props.text, 1920));
147
+ });
148
+
149
+ return (
150
+ <label
151
+ text={props.text}
152
+ width={width() || 600}
153
+ height={48}
154
+ color="0xFFFFFFFF"
155
+ font={<font uri={props.uri} size={32} ref={node => setFontRef(node.r)} />}
156
+ />
157
+ );
158
+ };
159
+
160
+ // Imperative form (same wait, no Solid helper):
161
+ async function measureOnce(ref: TmNodeRef, text: string): Promise<number> {
162
+ await whenFontReady(ref);
163
+ return getFontMeasure().getOneLineWidth(ref, text, 1920);
164
+ }
165
+ ```
166
+
167
+ ## Example
168
+
169
+ A row of chevrons and a rating, using a bundled typeface — imported, sized, and with a fallback
170
+ character for anything the typeface lacks.
171
+
172
+ ```tsx rsg-skill=complete
173
+ import { BLACK_STAR, LEFT_BUTTON_GLYPH, RIGHT_BUTTON_GLYPH, WHITE_STAR } from "@roku-sdk/components/glyphs";
174
+ import { Font, Label, LayoutGroup } from "@roku-sdk/components";
175
+
176
+ import titleFontUri from "./fonts/title.ttf";
177
+
178
+ function Rating(props: { filled: number; total: number }) {
179
+ return (
180
+ <label
181
+ text={BLACK_STAR.repeat(props.filled) + WHITE_STAR.repeat(props.total - props.filled)}
182
+ width={200}
183
+ height={48}
184
+ color="0xFFFFFFFF"
185
+ />
186
+ );
187
+ }
188
+
189
+ export function CarouselChrome(props: { title: string; rating: number }) {
190
+ return (
191
+ <layoutGroup layoutDirection="horiz" itemSpacings={[16]}>
192
+ {/* Private-use glyphs: the firmware fallback covers these even under a custom font. */}
193
+ <label text={LEFT_BUTTON_GLYPH} width={48} height={48} color="0xFFFFFFFF" />
194
+ <label
195
+ text={props.title}
196
+ width={600}
197
+ height={48}
198
+ color="0xFFFFFFFF"
199
+ // `size` is not optional in practice -- its default is 1 point,
200
+ // and a `Font` without a `uri` draws nothing at all.
201
+ font={<font uri={titleFontUri} size={32} />}
202
+ />
203
+ <Rating filled={props.rating} total={5} />
204
+ <label text={RIGHT_BUTTON_GLYPH} width={48} height={48} color="0xFFFFFFFF" />
205
+ </layoutGroup>
206
+ );
207
+ }
208
+ ```
209
+
210
+ ## Anti-patterns
211
+
212
+ Keyed by what you see on screen, because that is what arrives first.
213
+
214
+ | Symptom | Cause | Fix |
215
+ |---|---|---|
216
+ | Text shows empty boxes where symbols should be | The character is in neither font — geometric arrows like `◀` `▶` are the usual culprit | Use the named constant; `LEFT_ARROW` and friends are the shapes that exist |
217
+ | Arrows, ticks and dashes vanished after the app shipped its own typeface | Those are RokuUI-Regular symbols and get no RokuFont01 backstop under a custom font | Keep them in system-font text, or make the custom typeface supply them |
218
+ | Icons turned into unrelated shapes under a custom typeface | The custom font defines the same private-use codepoints and wins outright | Use a font without private-use glyphs, or draw those icons in system-font text |
219
+ | Text renders at size 1 — a readable string, unreadably small | `Font.size` was left at its default | Set `size` whenever you set `uri` |
220
+ | Changing the size does nothing at all | The `fontUri` names one of the eight fixed-size built-ins | Switch to `font:SystemFontFile` or a font file |
221
+ | Characters from a backend show as boxes and `fallbackGlyph` changed nothing | It has no visible effect on current firmware, in either value form | Constrain the text or widen the typeface; do not keep tuning the value |
222
+ | A label went completely blank after a `Font` node was added | The `Font` has a `size` but no `uri`, which draws nothing rather than falling back | Give it a `uri`, or drop the `Font` node and let the system font apply |
223
+ | The build fails naming two font files | Same filename, different contents, keyed by filename | Rename one of them |
224
+ | A private-use escape like `"\uE0nn"` sits in app code | The constant exists and the literal does not say which symbol it is | Import the constant |
225
+ | Braces render literally in a label | Firmware placeholder syntax used as app text | Interpolate the constant |
226
+ | `getFontMeasure` throws `"Font is not yet loaded"` or returns 0 | Measured before the GfxFont published; a fixed timer was too short | `await whenFontReady(ref)` or `createFontReady(fontRef)` — never a fixed timer |
227
+
228
+ The glyph groups here are device-verified against firmware 15.3.5 on a Roku Express 4K+ — every
229
+ constant rendered in a label under a custom typeface. Re-verify on a device rather than re-wording
230
+ if you need to change what they claim. `IFontProps` and `ISimpleLabelProps` carry TSDoc in the
231
+ installed package; prefer it over any prose, including this file, when the two disagree.
@@ -0,0 +1,128 @@
1
+ # Glyph tables
2
+
3
+ 45 constants, from `@roku-sdk/components/glyphs`. They are split into three tables here for the
4
+ one reason that matters at runtime — **which font supplies the character** — because that, not the
5
+ codepoint, decides whether a symbol survives a custom app typeface.
6
+
7
+ | Group | Count | Under a system font | Under a custom app font |
8
+ |---|---|---|---|
9
+ | RokuFont01 private-use glyphs | 32 | renders | renders — firmware fallback applies |
10
+ | Unicode characters RokuFont01 backs | 3 | renders | renders — same fallback |
11
+ | RokuUI-Regular symbols | 10 | renders | **tofu**, unless the custom font supplies them |
12
+
13
+ 35 of the 45 are safe under a custom typeface. The ten that are not are the ones that look most
14
+ ordinary, which is why this split is worth carrying.
15
+
16
+ Verified on device twice, on different hardware and different firmware, by rendering the
17
+ constants in a label with a custom TrueType font.
18
+
19
+ - Roku Express 4K+, firmware 15.3.5: the private-use glyphs and the stars came through; `←` `↑`
20
+ `→` `↓` `✓` `✗` were empty boxes. `•` `…` `–` `—` rendered only because that particular font
21
+ happened to contain them — do not read that as a guarantee.
22
+ - Roku Native (2910X), firmware 16.0.4, 1080p: same split. Under the system font every constant
23
+ rendered; under a custom TrueType the button glyphs and `★` `☆` rendered and `←` `→` `✓` were
24
+ boxes. The same run also showed a `Font` with a `uri` and no `size` drawing a speck, and
25
+ `fallbackGlyph` — as `u25CF` and as a literal `●` — leaving every box exactly as it was, both
26
+ for a RokuUI-only symbol and for a codepoint (`U+0E5B`, `U+A64B`) that no font on the device
27
+ has.
28
+
29
+ The two runs agree, which is the point of having both: this split is a property of the firmware's
30
+ font chain, not of one box.
31
+
32
+ ## 1. RokuFont01 — private-use glyphs (safe under a custom font)
33
+
34
+ RokuFont01 is wired in as the system-wide fallback font in firmware, so these resolve wherever
35
+ they appear.
36
+
37
+ | Constant | Codepoint | Placeholder | Description |
38
+ |---|---|---|---|
39
+ | `OPTIONS_GLYPH` | `U+E000` | `{OPTIONS_GLYPH}` | Options / star (no border) |
40
+ | `OK_BUTTON_GLYPH` | `U+E001` | `{OK_BUTTON_GLYPH}` | OK in circle |
41
+ | `OPTIONS_BUTTON_GLYPH` | `U+E002` | `{OPTIONS_BUTTON_GLYPH}` | Options / star (with border) |
42
+ | `MICROPHONE_GLYPH` | `U+E003` | `{MICROPHONE_GLYPH}` | Microphone (no border) |
43
+ | `PLAY_PAUSE_BUTTON_GLYPH` | `U+E004` | `{PLAY_PAUSE_BUTTON_GLYPH}` | Play/pause (with border) |
44
+ | `PLAY_PAUSE_GLYPH` | `U+E005` | `{PLAY_PAUSE_GLYPH}` | Play/pause (no border) |
45
+ | `DOT_GLYPH` | `U+E006` | `{DOT_GLYPH}` | Baseline-aligned dot (icon-row form) |
46
+ | `HOME_GLYPH` | `U+E007` | `{HOME_GLYPH}` | Home (no border) |
47
+ | `SEARCH_GLYPH` | `U+E008` | `{SEARCH_GLYPH}` | Search / magnifying glass |
48
+ | `REMOTE_GLYPH` | `U+E009` | `{REMOTE_GLYPH}` | Remote control |
49
+ | `REPLAY_GLYPH` | `U+E010` | `{REPLAY_GLYPH}` | Instant replay |
50
+ | `MICROPHONE_BUTTON_GLYPH` | `U+E011` | `{MICROPHONE_BUTTON_GLYPH}` | Microphone (with border) |
51
+ | `SEARCH_BUTTON_GLYPH` | `U+E012` | `{SEARCH_BUTTON_GLYPH}` | Search (with border) |
52
+ | `FAST_FORWARD_GLYPH` | `U+E013` | `{FAST_FORWARD_GLYPH}` | Fast forward |
53
+ | `REWIND_GLYPH` | `U+E014` | `{REWIND_GLYPH}` | Rewind |
54
+ | `SHORTCUT_ONE_BUTTON_GLYPH` | `U+E015` | `{SHORTCUT_ONE_BUTTON_GLYPH}` | Shortcut 1 |
55
+ | `SHORTCUT_TWO_BUTTON_GLYPH` | `U+E016` | `{SHORTCUT_TWO_BUTTON_GLYPH}` | Shortcut 2 |
56
+ | `CLOSED_CAPTIONS_GLYPH` | `U+E017` | `{CLOSED_CAPTIONS_GLYPH}` | Closed captions |
57
+ | `LIVE_TV_GLYPH` | `U+E018` | `{LIVE_TV_GLYPH}` | Live TV |
58
+ | `SLEEP_TIMER_GLYPH` | `U+E019` | `{SLEEP_TIMER_GLYPH}` | Sleep timer |
59
+ | `GENERAL_APP_GLYPH` | `U+E020` | `{GENERAL_APP_GLYPH}` | General app icon |
60
+ | `BACK_BUTTON_GLYPH` | `U+E021` | `{BACK_BUTTON_GLYPH}` | Back |
61
+ | `HOME_BUTTON_GLYPH` | `U+E022` | `{HOME_BUTTON_GLYPH}` | Home button |
62
+ | `QUICK_LAUNCH_BUTTON_GLYPH` | `U+E023` | `{QUICK_LAUNCH_BUTTON_GLYPH}` | Quick launch / rocket |
63
+ | `DOWN_BUTTON_GLYPH` | `U+E024` | `{DOWN_BUTTON_GLYPH}` | Down |
64
+ | `LEFT_BUTTON_GLYPH` | `U+E025` | `{LEFT_BUTTON_GLYPH}` | Left |
65
+ | `RIGHT_BUTTON_GLYPH` | `U+E026` | `{RIGHT_BUTTON_GLYPH}` | Right |
66
+ | `KIDS_PROFILE_GLYPH` | `U+E027` | `{KIDS_PROFILE_GLYPH}` | Kids profile |
67
+ | `UP_BUTTON_GLYPH` | `U+E028` | `{UP_BUTTON_GLYPH}` | Up |
68
+ | `POWER_BUTTON_GLYPH` | `U+E029` | `{POWER_BUTTON_GLYPH}` | Power |
69
+ | `THUMBS_UP_GLYPH` | `U+E030` | `{THUMBS_UP_GLYPH}` | Thumbs up |
70
+ | `THUMBS_DOWN_GLYPH` | `U+E031` | `{THUMBS_DOWN_GLYPH}` | Thumbs down |
71
+
72
+ The numbering is decimal-style: the A–F slots in each decade (`U+E00A`–`U+E00F`, `U+E01A`–`U+E01F`,
73
+ `U+E02A`–`U+E02F`) are unassigned. The font defines 32 private-use codepoints and the table lists
74
+ all of them.
75
+
76
+ `DOT_GLYPH` (`U+E006`) and `BLACK_CIRCLE` (`U+25CF`) are the same outline at two vertical
77
+ positions — `DOT_GLYPH` sits on the baseline to line up with the other icon glyphs, `BLACK_CIRCLE`
78
+ is raised to text centre for use inside a run of text. Both have full outline data; neither is
79
+ reserved.
80
+
81
+ ## 2. Unicode characters RokuFont01 backs (safe under a custom font)
82
+
83
+ Real Unicode codepoints, but RokuUI-Regular does not contain them — they resolve through the same
84
+ RokuFont01 fallback as the private-use glyphs above. That makes them the *safe* shapes to use in
85
+ custom-font text.
86
+
87
+ | Constant | Codepoint | Char | Description |
88
+ |---|---|---|---|
89
+ | `BLACK_CIRCLE` | `U+25CF` | ● | Dot raised to text centre, for use inside a text run |
90
+ | `BLACK_STAR` | `U+2605` | ★ | Filled star |
91
+ | `WHITE_STAR` | `U+2606` | ☆ | Outlined star |
92
+
93
+ ## 3. RokuUI-Regular symbols (no backstop under a custom font)
94
+
95
+ Part of the default UI font, so they need no fallback under a system font. Under a custom app font
96
+ there is no RokuFont01 backstop for them either. **These are the ten constants to audit if your
97
+ app ships its own typeface.**
98
+
99
+ | Constant | Codepoint | Char | Description |
100
+ |---|---|---|---|
101
+ | `BULLET` | `U+2022` | • | Bullet |
102
+ | `ELLIPSIS` | `U+2026` | … | Horizontal ellipsis |
103
+ | `EN_DASH` | `U+2013` | – | En dash |
104
+ | `EM_DASH` | `U+2014` | — | Em dash |
105
+ | `LEFT_ARROW` | `U+2190` | ← | Leftwards arrow |
106
+ | `UP_ARROW` | `U+2191` | ↑ | Upwards arrow |
107
+ | `RIGHT_ARROW` | `U+2192` | → | Rightwards arrow |
108
+ | `DOWN_ARROW` | `U+2193` | ↓ | Downwards arrow |
109
+ | `CHECK_MARK` | `U+2713` | ✓ | Check mark |
110
+ | `BALLOT_X` | `U+2717` | ✗ | Ballot X |
111
+
112
+ ## What has no constant, on purpose
113
+
114
+ Characters you can type directly get none: `·` middle dot, `°` degree, currency signs, maths
115
+ operators, quotation marks. Type them. A constant would add a name to look up without adding
116
+ anything the compiler or the device cares about.
117
+
118
+ Geometric arrows `◀` and `▶` are in **neither** font and have no constants because they do not
119
+ render. Reach for `LEFT_ARROW` / `RIGHT_ARROW`, or the bordered `LEFT_BUTTON_GLYPH` /
120
+ `RIGHT_BUTTON_GLYPH` when you want the remote-button look.
121
+
122
+ ## Firmware placeholders
123
+
124
+ The `{CONSTANT_NAME}` column above is the firmware's own placeholder syntax, used in translated
125
+ firmware strings and resolved by the firmware's string layer. It is not a TypeScript symbol and it
126
+ is not template syntax: putting `{BACK_BUTTON_GLYPH}` in a label renders those braces literally.
127
+ Use the placeholder form only where you are explicitly producing text for something that resolves
128
+ firmware placeholders, and interpolate the imported constant everywhere else.
@@ -0,0 +1,227 @@
1
+ # Logging and Errors
2
+
3
+ ## Overview
4
+
5
+ An RSG SDK TypeScript app reports what it is doing through one exported `logger`, and survives a
6
+ throw through Solid's `ErrorBoundary`. Both have a platform edge that catches people out: log
7
+ levels are enforced in **two** places, one of which deletes your call before the app ever runs,
8
+ and the SDK already wraps every screen in a boundary whose fallback draws nothing — so the
9
+ default shape of a failure here is a blank screen rather than a crash.
10
+
11
+ ## When to Use
12
+
13
+ - a log level raised and nothing new appears
14
+ - log levels configured somewhere that turns out to have no effect
15
+ - `console` calls present while developing and gone from the build handed over
16
+ - a screen that comes up blank with no crash and no stack
17
+ - containing a throw so one bad screen does not take the app with it, or recovering afterwards
18
+ - an uncaught-exception handler that never fires, or fires and then behaves unexpectedly
19
+ - a crash stack that cannot be traced back to the TypeScript that produced it
20
+
21
+ ### When not to use
22
+
23
+ | Situation | Use instead | Because |
24
+ |---|---|---|
25
+ | Getting logs **off** a device from a shell script or a CI job | The `cli-dev-loop` skill | This skill is about a line being emitted; collecting it afterwards is a different surface |
26
+ | Getting logs off a device, or decoding a stack, inside an agent session | The `mcp-ai-dev-loop` skill | Same distinction, reached through tool calls rather than a shell |
27
+ | What the running app costs in frames, memory or startup time | The `app-performance` skill | Instrumenting for cost is measurement, not reporting |
28
+ | Which Solid primitives `@roku-sdk/solid` exports at all | The `solid-on-roku` skill | This skill owns the failure, not the export surface |
29
+
30
+ ## Read the reference first for this task
31
+
32
+ | If the request is | Read now, before answering |
33
+ |---|---|
34
+ | Writing or debugging a `setUncaughtExceptionHandler` callback | `references/uncaught-exceptions.md` |
35
+
36
+ The tables below say that the handler exists, what it never sees, and that registering one cannot
37
+ be undone. Its timeout, its inverted `done` flag and the conditions under which it silently does
38
+ nothing are only in the reference, and a handler written from the summary alone is wrong in ways
39
+ that do not show up until something throws in production.
40
+
41
+ ## Quick Reference
42
+
43
+ | Question | Answer |
44
+ |---|---|
45
+ | Get a logger | `import { logger } from "@roku-sdk/rsg-ts"` — the root one; `logger.getLogger("myscreen")` for a child |
46
+ | Levels available | `trace`, `debug`, `info`, `warn`, `error`. There is no `fatal`, and `createLogger` is not exported |
47
+ | Why a message is a function | `logger.info(() => \`...\`)` — the thunk is only invoked once the level check passes, so a filtered line costs nothing to build |
48
+ | The two level knobs | `app.logger.baseLogLevel` **deletes** call sites at build time (default INFO in production builds); `app.logger.currentLogLevel` filters what remains at runtime (default ERROR; also enforced as a runtime backstop at the base level) |
49
+ | Which one to raise | Whichever is higher wins. Raising only `currentLogLevel` recovers nothing a lower `baseLogLevel` already deleted |
50
+ | Setting levels via `buildVariables` | A silent no-op — the build overwrites `__BASE_LOG_LEVEL__` and `__CURRENT_LOG_LEVEL__` itself |
51
+ | Contain a throw | `<ErrorBoundary fallback={…}>` around the subtree, from `@roku-sdk/solid` |
52
+ | Recover from one | `catchError` for a scoped throw, `resetErrorBoundaries` to retry every boundary |
53
+ | Blank screen, no crash, no stack | The SDK's own boundary caught a root-component throw. One `error` line under source `btstp` is the entire evidence |
54
+ | `console` in a shipped build | Dropped by `rk build`, kept by `rk dev`; `dropConsole: false` keeps them, and then `installConsolePolyfill()` restores the helpers |
55
+ | Trace a stack back to TypeScript | Call `enableExceptionTagging()` before the code that can throw. Inside `main()`, before the anchor node, covers throws after `main()`; a throw at module top level needs the call in the entry file's first import |
56
+ | An uncaught-exception handler | `setUncaughtExceptionHandler` — never sees an `ErrorBoundary` catch, sees nothing thrown before it registers, and registration is permanent |
57
+ | Where these live | `logger`, `main`, `enableExceptionTagging` and `setUncaughtExceptionHandler` all come from `@roku-sdk/rsg-ts`; `installConsolePolyfill` only from `@roku-sdk/native` |
58
+
59
+ ## Instructions
60
+
61
+ 1. **Log through the exported `logger`, and give each area a child.** `logger` is the root;
62
+ `logger.getLogger("playback")` tags that area's lines. `createLogger` is deliberately module
63
+ private, so a child logger is the only sanctioned way to get a distinct source.
64
+
65
+ 2. **Pass a thunk, not a string.** Every method takes `() => string`. The level check runs first,
66
+ so an interpolation inside the thunk is never paid for on a filtered line. Building the string
67
+ at the call site throws that away.
68
+
69
+ 3. **Understand which level does what, because only one of them can be undone.**
70
+ - `baseLogLevel` is a **build-time** cut. Any statement calling a method below it is replaced
71
+ with `void 0;` in the source before the bundler sees it, so the call is not in the artifact
72
+ at all. Nothing at runtime can bring it back.
73
+ - `currentLogLevel` is a **runtime** filter over whatever survived, resolved once at module
74
+ load.
75
+
76
+ So diagnose in that order: if a line is missing, ask whether it was deleted before asking
77
+ whether it was filtered. The defaults are `INFO` for the base cut and `ERROR` for the runtime
78
+ filter, which is why a `debug` line is absent from a default build twice over.
79
+
80
+ 4. **Know what the build-time cut checks and what it does not.** It matches a whole expression
81
+ statement whose call target is a log method (e.g. `.trace`, `.debug`) on a receiver that is
82
+ recognised as an SDK logger binding in that file. Specifically, the receiver must be:
83
+ - A named import of `logger` (or an aliased import) from one of the SDK logger modules
84
+ (`@roku-sdk/rsg-ts` or `@roku-sdk/common`), or common's own relative `./logger` import.
85
+ - A child logger derived from any of the above via `const X = logger.getLogger("area")`
86
+ (and any further `.getLogger(…)` chains at module level).
87
+ - An inline `logger.getLogger("a").debug(…)` call in statement position.
88
+
89
+ Non-SDK receivers (`someLibrary.debug(…)`, `console.debug(…)`, `myEmitter.trace(…)`) are
90
+ never removed. Shadowing is respected: when a local binding reuses the name — any parameter
91
+ (constructor, accessor and destructured ones included), a `catch` binding, a loop variable, or
92
+ a local declaration — calls through it are left untouched.
93
+
94
+ One documented limitation: a logger exported from one module and re-imported under a
95
+ *different* import path (not one of the recognised specifiers) is not recognised by the
96
+ per-file pass and is not stripped at build time. The runtime backstop (see below) still
97
+ silences it. The sanctioned pattern is: import `logger` from the SDK and derive children
98
+ with `getLogger` in the same module.
99
+
100
+ A second limitation: inside a function, only a child derived directly from a recognised SDK
101
+ logger is stripped. A grandchild built from a function-local child — `const a =
102
+ logger.getLogger("a"); const b = a.getLogger("b"); b.debug(…)`, or inline
103
+ `a.getLogger("b").debug(…)` — is kept at build time. Like the case above, this only ever
104
+ leaves a call in (never removes a non-SDK one), and the runtime backstop still silences it.
105
+ Derive nested children at module level if you want them stripped.
106
+
107
+ Child loggers must be declared with `const`. A `let` or `var` child
108
+ (`let l = logger.getLogger("x")`) can be reassigned to some other object, so its calls are
109
+ kept at build time rather than risk deleting app code. The runtime backstop still silences them.
110
+
111
+ Only a bare statement is stripped. `const line = logger.debug(…)`, or a call guarded by
112
+ `&&`, survives — so a log statement written in an unusual shape will not be stripped as
113
+ expected.
114
+
115
+ 5. **Set levels in `app.logger`, never in `buildVariables`.** The two keys the logger reads are
116
+ `__BASE_LOG_LEVEL__` and `__CURRENT_LOG_LEVEL__`, and the build writes both itself —
117
+ unconditionally, after copying your `buildVariables` — then restores them only from
118
+ `app.logger`. So a value you put in `buildVariables` under either name is overwritten with an
119
+ empty string, which the reader treats exactly like an unset variable. You get the defaults,
120
+ with no warning that your setting was discarded.
121
+
122
+ 6. **Wrap a subtree that can throw in `ErrorBoundary`, and draw something in the fallback.** The
123
+ SDK already puts one boundary around the whole app, and its fallback returns nothing — so an
124
+ uncontained throw in a root component is a blank screen rather than a crash. That is the SDK
125
+ working as designed, not a missing feature. Your own boundary, closer to the risk, is what
126
+ turns it into a message the user can act on.
127
+
128
+ 7. **Recover with `catchError` or `resetErrorBoundaries`.** `catchError` handles a throw inside
129
+ one computation without unmounting a subtree; `resetErrorBoundaries` re-runs every boundary's
130
+ children, which is the "try again" button's implementation.
131
+
132
+ 8. **Expect `console` to disappear from the build you hand over.** `rk build` drops `console.*`
133
+ and `debugger`; `rk dev` keeps them. Set `dropConsole: false` when a build must keep them
134
+ on purpose — and note the helper methods (`count`, `group`, `assert` and the timing pair) come
135
+ from a polyfill that only auto-installs in a development build, so a production build that
136
+ keeps `console` also needs `installConsolePolyfill()` from `@roku-sdk/native`. Plain
137
+ `console.log`, `warn` and `error` are the runtime's own and are never wrapped. None of this
138
+ replaces `logger`: `console` has no levels, no sources and no build-time cut.
139
+
140
+ 9. **Call `enableExceptionTagging()` before the code that can throw**, when traces from this
141
+ build have to be resolvable later. Inside `main()`, before registering the anchor node, covers
142
+ throws from the `main()` callback onward. A throw while the bundle is still being evaluated
143
+ needs the call in the same module the entry file imports first (step 10). It marks each trace
144
+ with the build's source-map identity. It is idempotent, and calling it costs nothing at runtime
145
+ beyond the tagging itself. Turning a captured stack back into TypeScript afterwards is
146
+ `cli-dev-loop` from a shell and `mcp-ai-dev-loop` from a session — this step is only the app's half.
147
+
148
+ 10. **Reach for `setUncaughtExceptionHandler` only for what a boundary cannot catch**, and read
149
+ the reference before writing one. It replaces the SDK's default permanently, it never sees a
150
+ throw an `ErrorBoundary` already caught, and it is not the mechanism behind the blank screen
151
+ in step 6. It also misses anything thrown before it registers, and every module's top-level
152
+ code runs before `main()` is called. When a module can throw at its top level, register in
153
+ the entry file's first import rather than inside `main()`.
154
+
155
+ ## Render-thread transaction errors
156
+
157
+ On firmware with `roku.Transaction.setErrorCallback`, SDK `main()` installs a default
158
+ error-level logger under source `tx` before declaring app components. It reports each
159
+ render-thread transaction error with its reason, message and available operation, node ID
160
+ and property name. These asynchronous errors do not throw through `ErrorBoundary` or the
161
+ uncaught-exception handler. Apps calling `roku.Transaction.setErrorCallback` themselves
162
+ replace this default; accessors are valid only during that callback. Older firmware without
163
+ the callback API is unchanged and cannot surface these errors through this SDK handler.
164
+
165
+ ## Example
166
+
167
+ An app entry point that tags its traces, and a screen that contains its own failures instead of
168
+ letting the SDK's blank fallback have them.
169
+
170
+ ```tsx rsg-skill=complete
171
+ import { Label, LayoutGroup } from "@roku-sdk/components";
172
+ import { type AnchorNodeImplementation, defineAnchorNode, FieldType, logger } from "@roku-sdk/rsg-ts";
173
+ import { ErrorBoundary } from "@roku-sdk/solid";
174
+
175
+ const screenLog = logger.getLogger("playback");
176
+
177
+ const PlaybackScreenDescription = {
178
+ componentName: "PlaybackScreen",
179
+ extends: "Group" as const,
180
+ inputFields: {
181
+ assetId: FieldType.String("")
182
+ },
183
+ outputFields: {}
184
+ };
185
+
186
+ function NowPlaying(props: { assetId: string }) {
187
+ // The thunk is only invoked once the level check passes.
188
+ screenLog.info(() => `rendering asset ${props.assetId}`);
189
+ return <label text={props.assetId} width={800} height={48} color="0xFFFFFFFF" />;
190
+ }
191
+
192
+ const PlaybackScreen: AnchorNodeImplementation<typeof PlaybackScreenDescription> = inputs => (
193
+ <layoutGroup layoutDirection="vert">
194
+ <ErrorBoundary
195
+ fallback={error => {
196
+ // Without a fallback that draws, this subtree is simply absent.
197
+ screenLog.error(() => `playback failed: ${error.message}`);
198
+ return <label text="Playback unavailable" width={800} height={48} color="0xFF6666FF" />;
199
+ }}
200
+ >
201
+ <NowPlaying assetId={inputs.assetId ?? ""} />
202
+ </ErrorBoundary>
203
+ </layoutGroup>
204
+ );
205
+
206
+ export default defineAnchorNode(PlaybackScreenDescription, PlaybackScreen);
207
+ ```
208
+
209
+ ## Common Mistakes
210
+
211
+ | Symptom | Cause | Fix |
212
+ |---|---|---|
213
+ | A level was raised, the app rebuilt, and the lines are still missing | `baseLogLevel` deleted the call sites; raising the runtime filter cannot restore a call that is not in the artifact | Raise `app.logger.baseLogLevel` and rebuild |
214
+ | Levels set through `buildVariables` change nothing, and nothing warns | The build overwrites `__BASE_LOG_LEVEL__` and `__CURRENT_LOG_LEVEL__` with an empty string before restoring them from `app.logger` | Set them in `app.logger` |
215
+ | A screen is blank, the app is still running, and there is no stack | A root component threw and the SDK's own boundary caught it; its fallback draws nothing | Search the log for source `btstp`; add your own `ErrorBoundary` closer to the risk |
216
+ | An `ErrorBoundary` was added and the screen is still blank | The `fallback` returns nothing, or the boundary sits above the component that throws rather than around it | Return a real node from `fallback`; put the boundary around the risky subtree |
217
+ | An uncaught-exception handler never fires for a render-time throw | A boundary caught it first, and the handler only sees what nothing caught | Handle it in the boundary; keep the handler for what escapes |
218
+ | The app exits from the splash screen, and the handler never fires | A module threw at its top level while the bundle was evaluated, before the handler was registered inside `main()` | Register in a module the entry file imports first, as a bare `import "./…"` |
219
+ | A registered handler cannot be swapped back to the SDK default | Registration replaces the default permanently, by design | Register once, at startup, and branch inside your own handler |
220
+ | A handler was registered and does nothing at all, silently | `globalThis.Process` was unavailable, so the call warned and returned — and at the default runtime filter that warning is itself invisible | Read the reference; confirm the handler registers where the runtime provides it |
221
+ | Diagnostics print in development and are gone from the shipped build | `rk build` drops `console.*` by default | Use `logger`, or set `dropConsole: false` deliberately |
222
+ | `console` timing and grouping helpers are missing in a build that kept `console` | The polyfill auto-installs only in a development build | Call `installConsolePolyfill()` from `@roku-sdk/native` |
223
+ | A captured stack cannot be matched to a build | The app never tagged its traces, or tagged them inside `main()` after a module had already thrown | Call `enableExceptionTagging()` before that code. Inside `main()` covers throws after it; a module top-level throw needs the call in the entry file's first import |
224
+
225
+ `Logger`, `setUncaughtExceptionHandler` and `ErrorBoundary` are typed with TSDoc in the installed
226
+ packages. Read an option there rather than guessing its name, and prefer it over any prose,
227
+ including this file, when the two disagree.