@roku-sdk/ai 2.18.0 → 2.19.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roku-sdk/ai",
3
- "version": "2.18.0",
3
+ "version": "2.19.1",
4
4
  "description": "AI skills library and manifest for the Roku RSG TypeScript SDK",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -19,13 +19,13 @@
19
19
  "node": ">=24.0.0"
20
20
  },
21
21
  "dependencies": {
22
- "zod": "^3.23.8"
22
+ "zod": "^4.6.6"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@jest/globals": "^30.4.1",
26
26
  "@types/jest": "^30.0.0",
27
27
  "jest": "^30.4.1",
28
- "@roku-sdk/dev-tools": "2.18.0"
28
+ "@roku-sdk/dev-tools": "2.19.1"
29
29
  },
30
30
  "author": "Roku Inc",
31
31
  "license": "MIT",
@@ -103,11 +103,13 @@ const pulse = <effect id="tile-pulse" borderRadius={24} borderWidth={2} borderCo
103
103
  6. **An Effect clips nothing.** A child of a rounded `Rectangle` can paint into and past the
104
104
  corners. Lay children out inside the rounded area; to clip them to a shape, use `MaskGroup`.
105
105
 
106
- 7. **Know the fields that fail silently.** A gradient draws nothing while `gradientStyle` is
107
- `"none"`, its default, or with fewer than two or more than eight `gradientColors`.
108
- `gradientStops` needs one entry per colour, and a mismatch is not reported. `borderPadding` is
109
- the gap between the border and the content, so with `borderWidth` at `0` it changes nothing —
110
- inset a child with layout instead. `gradientFillContent` defaults to `true` and
106
+ 7. **Know the fields that fail silently on device.** A gradient draws nothing while
107
+ `gradientStyle` is `"none"`, its default, or with fewer than two or more than eight
108
+ `gradientColors`. `gradientStops` needs one entry per colour in `[0, 1]`. In `__DEV__` builds
109
+ the SDK warns once per distinct problem (and stays quiet while `gradientStyle` is `"none"`,
110
+ including empty colors/stops). `borderPadding` is the gap between the border and the content,
111
+ so with `borderWidth` at `0` it changes nothing — inset a child with layout instead.
112
+ `gradientFillContent` defaults to `true` and
111
113
  `gradientFillBorder` to `false`.
112
114
 
113
115
  8. **`setPropMethod("effect")` is not how you share.** It builds a property descriptor for
@@ -57,7 +57,7 @@ Use this whenever the question is which item is focused, or what a key press doe
57
57
  | Move focus from app logic | `useFocusActions()` → `setFocus(id)`; `focusedId()` to read it |
58
58
  | Act on a key nobody handled, such as `back` | `onUnhandledKey` on the boundary |
59
59
  | Reopen a region at its first item | `rememberLastFocus={false}` — the default `true` restores the last item |
60
- | Mark the item an inactive region will return to — dialog open, or focus in another region | `hasFootprint()` from `useFocusable` |
60
+ | Mark the item an inactive region will return to — dialog open, or focus in another region | `hasFootprint()` from `useFocusable`; `hasReturnFootprint()` when nested regions or `rememberLastFocus={false}` must mark only where focus will land, and `useFocusActions().hasReturnFootprint()` for the region itself |
61
61
 
62
62
  ## Instructions
63
63
 
@@ -70,6 +70,11 @@ app's other `@roku-sdk/*` packages are on — or every import below fails to res
70
70
  pass all three to `RootFocusBoundary`. Without this no key reaches TypeScript and nothing
71
71
  moves. One per anchor node — never give a nested boundary its own `keyEvent`.
72
72
 
73
+ For TypeScript descriptors, keep `FocusBoundaryBase`: registration selects the native
74
+ component when firmware reports `hasFeature("focusBoundaryBase")`, otherwise
75
+ `FocusBoundaryBaseShim`. XML/BrightScript does not go through that remap: extend or
76
+ create `FocusBoundaryBaseShim` by name to keep working on existing firmware.
77
+
73
78
  2. **Wrap each region in a `FocusBoundary`** and set `navigation` to the axis the region
74
79
  moves on. Nest a region by giving it a `focusId` that its parent's `initialFocus` or
75
80
  `navigationMap` names.
@@ -271,6 +271,14 @@ sibling region is another. A row styled from it therefore keeps its subdued mark
271
271
  is over in the menu — usually what you want, but decide it rather than discover it. To mark only
272
272
  the dialog case, pass the dialog's `isOpen()` down and gate the marker on it.
273
273
 
274
+ It is also evaluated against its own boundary alone, so a nested region focus will not pass back
275
+ through keeps marking its last child, and so does a region with `rememberLastFocus={false}` that
276
+ will reopen at `initialFocus`. When only items focus will actually land on should be marked,
277
+ use `hasReturnFootprint()` from `useFocusable` instead; `useFocusActions().hasReturnFootprint()`
278
+ answers the same question for the whole region. Each dormant sibling region under the nearest
279
+ active ancestor still marks its own item — the one focus reaches when the user moves there — so
280
+ the tree shows a single footprint only while the root itself is inactive.
281
+
274
282
  ### Common mistakes
275
283
 
276
284
  | Symptom | Cause | Fix |
@@ -48,6 +48,7 @@ mistake here that compiles.
48
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
49
  | Ship a typeface with the app | `import fontUri from "./fonts/whatever.ttf"` — the import resolves to the packaged path at build time |
50
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 |
51
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 |
52
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 |
53
54
  | Which symbols survive a custom app font | The 32 private-use glyphs and `●` `★` `☆`. The other ten do not |
@@ -110,6 +111,59 @@ mistake here that compiles.
110
111
  firmware's own string layer, not TypeScript symbols; putting one in a label renders the braces
111
112
  literally. Interpolate the imported constant instead.
112
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
+
113
167
  ## Example
114
168
 
115
169
  A row of chevrons and a rating, using a bundled typeface — imported, sized, and with a fallback
@@ -169,6 +223,7 @@ Keyed by what you see on screen, because that is what arrives first.
169
223
  | The build fails naming two font files | Same filename, different contents, keyed by filename | Rename one of them |
170
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 |
171
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 |
172
227
 
173
228
  The glyph groups here are device-verified against firmware 15.3.5 on a Roku Express 4K+ — every
174
229
  constant rendered in a label under a custom typeface. Re-verify on a device rather than re-wording
@@ -58,7 +58,7 @@ that session's whole life, and the reference is how you reach a frame worth read
58
58
  | Sideload an existing artifact | `sideload_app` | Does not build. Reads the directory you point it at |
59
59
  | See the screen | `capture_screen` | Returns an image, and writes a file |
60
60
  | Read the node tree | `ecp` with `command: "query/app-ui"` | The rendered tree of the running app, as JSON |
61
- | Read device facts | `ecp` with `command: "query/device-info"` | Model, firmware, resolution |
61
+ | Read device facts | `ecp` with `command: "query/device-info"` | Model, firmware, resolution; an SDK app needs `software-version` 16.0 or later |
62
62
  | Press a key | `ecp` with `command: "keypress"` | One key |
63
63
  | Press several | `ecp` with `command: "keysequence"` | One call for a whole path through the UI. Prefer it over a run of single presses |
64
64
  | Name a key for either of those | The **wire** name, not the runtime one | `Select` (not `OK`), `Rev` (not `rewind`), `Fwd` (not `fastforward`), `Info` (not `options`). Every element is validated before anything reaches the device, so one wrong name fails the whole call with `ECP_INVALID_KEY` |
@@ -102,7 +102,8 @@ that session's whole life, and the reference is how you reach a frame worth read
102
102
  actually found. A project that sets `outDir` in `roku-config.ts` avoids the split entirely:
103
103
  the sideload path reads project config, so an `outDir` from config or from the parameter is
104
104
  treated as deliberate and draws no warning. Sideloading Hermes bytecode (`index.hbc`) also
105
- returns a warning, since a device without the `rsg_typescript` token refuses to launch it.
105
+ returns a warning, since a device without the `rsg_typescript` token refuses to launch it, and so does
106
+ installing to a device older than Roku OS 16.0, naming the version it runs.
106
107
 
107
108
  5. **Never reach the device's network endpoints yourself.** No `curl`, no hand-built request
108
109
  to a device port. The tools carry the device's password, retry sensibly and report failure
@@ -178,6 +179,7 @@ observation after that point describes a device that is no longer running your a
178
179
  | The device is running older code than the source | `build_app` wrote `dist-build` and a bare `sideload_app` read `dist-dev` | Use `deploy_app`, or pass `outDir: "dist-build"` to `sideload_app`. The sideload now warns when the directory it read is older than the other one |
179
180
  | A tool reports no device | Nothing registered, or nothing active | `list_devices`, then `activate_device`; `add_device` if the list is empty |
180
181
  | Sideloading is refused | Developer mode is off, or the device password is wrong | Enable developer mode on the box, then set the password with `update_default_auth` |
182
+ | The install succeeds but the app never runs, on a box where other apps do | The device is on a Roku OS older than 16.0, the minimum an SDK app needs | The install's `warnings` name the version; otherwise `ecp` with `command: "query/device-info"` and read `software-version`. The device's software has to be updated, which no tool does |
181
183
  | `logs` fails before it reads anything | It requires server mode, and there was no session | Start `rk server`, then `logs` with `action: "start"` before `get` or `search` |
182
184
  | Keys appear to do nothing | The app crashed, or focus is not where you assumed | `capture_screen`, then `ecp` with `query/app-ui`; check the logs for an exception |
183
185
  | A screenshot looks right but the test of it fails | Measured in device pixels, compared against design coordinates | Convert before comparing |
@@ -7,6 +7,10 @@ runs. Five things then own the project: `roku-config.ts` (app config and manifes
7
7
  `tsconfig.json` (one `extends` line), `package.json` (the `@roku-sdk/*` dependencies),
8
8
  `src/` (TypeScript and imported images) and `pkg/` (static assets copied verbatim).
9
9
 
10
+ An SDK app runs on **Roku OS 16.0 or later**. Below 16.0 a device runs one only with a
11
+ Roku-issued authorization token, so update the device's software rather than working around
12
+ it. A feature with a higher floor of its own says so where it is documented.
13
+
10
14
  ## Read the reference first for these tasks
11
15
 
12
16
  | If the request is | Read now, before answering |
@@ -28,6 +32,7 @@ there is nothing in it to answer from.
28
32
  - the app's TypeScript setup is wrong: JSX or SDK types not resolving
29
33
  - declaration generation stops on an XML component base the SDK does not map, or whether built-in mappings still apply alongside yours in `dtsOptions`
30
34
  - `rk` cannot find the config, or a first run on a device is not working yet
35
+ - which Roku OS version a device needs before an app will run on it
31
36
 
32
37
  ### When not to use
33
38
 
@@ -46,6 +51,7 @@ there is nothing in it to answer from.
46
51
  | Task | Do this |
47
52
  |---|---|
48
53
  | Create an app | `npx rk create my-app` — name lowercase, letters, digits, hyphens |
54
+ | Check a device can run the app | `npx rk ecp query/device-info --json` — `software-version` must be 16.0 or later |
49
55
  | Change title, version, splash, resolutions | `manifestEntries` in `roku-config.ts` |
50
56
  | An XML base declaration generation does not recognise | `dtsOptions.componentMapping` in `roku-config.ts`; keep sibling `includeDefaults` on so the SDK's own mappings still merge |
51
57
  | Configure anything else in `roku-config.ts` | `references/roku-config.md` — the field names are only there |
@@ -202,6 +208,7 @@ and that direction belongs to the `mcp-ai-dev-loop` skill.
202
208
  | `.roku/` is git-ignored but `rk` still warns that it is not | The check recognises `.roku`, `/.roku`, `.roku/` and `/.roku/`, and no other pattern | Use one of those four forms rather than a glob such as `**/.roku` |
203
209
  | The build stops on an XML component whose base the declaration generation does not recognise, and no SDK version ever mapped it | The base is the app's own, so only the app can declare it | Add it under `dtsOptions.componentMapping` in `roku-config.ts`, and leave the sibling `includeDefaults` on so the SDK's own mappings still merge in |
204
210
  | `rk` cannot find the config | It walks up from the current directory | Run from inside the app, or pass `--config` |
211
+ | The app builds, but the device will not run it | The device is on a Roku OS older than 16.0 | Read `software-version` from `npx rk ecp query/device-info`; update the device's software, then install again |
205
212
  | The build fails on something the installed version handles — a base component the SDK maps itself, a define it sets itself — or otherwise behaves like an older SDK | `rk` prefers a running `rk server`, which executes the build with the tooling it loaded at startup, from whatever project it was started in. Reinstalling the app's packages cannot change code already resident in that process | `rk server status` to see it, `rk server stop` then start it again from this app, or pass `--local` to skip server discovery for one command |
206
213
  | A package you import is missing at build time | pnpm does not hoist it in | Declare it in the app's own `package.json` |
207
214
  | An import of `solid-js` does not resolve, or its API does nothing on the device | `solid-js` belongs to `@roku-sdk/solid`, not to the app, and only the re-exported part drives the SceneGraph renderer | Import from `@roku-sdk/solid` |
@@ -125,12 +125,14 @@
125
125
  },
126
126
  {
127
127
  "name": "project-setup",
128
- "description": "Use when an RSG SDK TypeScript app has to be created or its project-level configuration is wrong \u2014 scaffolding a new app, what roku-config.ts owns (title, version, splash, manifest entries, and dtsOptions such as includeDefaults when declaration generation does not recognise an XML base), the app's tsconfig.json, which @roku-sdk packages the app must list because its own code imports them, what the scaffold's .gitignore covers after rk create, or where a file such as an image has to live before it can be imported in code. Not for resolving SDK packages from npmjs.com, which version line installs, or a peer an SDK package requires the app to declare, which is the sdk-installation skill's subject.",
128
+ "description": "Use when an RSG SDK TypeScript app has to be created or its project-level configuration is wrong \u2014 scaffolding a new app, what roku-config.ts owns (title, version, splash, manifest entries, and dtsOptions such as includeDefaults when declaration generation does not recognise an XML base), the app's tsconfig.json, which @roku-sdk packages the app must list because its own code imports them, what the scaffold's .gitignore covers after rk create, which Roku OS version a device needs before an app runs on it (16.0 or later), or where a file such as an image has to live before it can be imported in code. Not for resolving SDK packages from npmjs.com, which version line installs, or a peer an SDK package requires the app to declare, which is the sdk-installation skill's subject.",
129
129
  "triggers": [
130
130
  "roku-config",
131
131
  "scaffold",
132
132
  "tsconfig",
133
- "rk create"
133
+ "rk create",
134
+ "minimum roku os",
135
+ "roku os version"
134
136
  ],
135
137
  "path": "skills/project-setup/SKILL.md",
136
138
  "references": [
@@ -30,6 +30,12 @@ describe("trigger routing — shipped manifest routes cleanly", () => {
30
30
  expect(router.route("FocusBoundary")).toBe("focus-management");
31
31
  });
32
32
 
33
+ it("routes a question about the minimum Roku OS to project-setup", () => {
34
+ const router = buildRouter(shippedManifest());
35
+ expect(router.route("minimum Roku OS")).toBe("project-setup");
36
+ expect(router.route("Roku OS version")).toBe("project-setup");
37
+ });
38
+
33
39
  it("routes case-insensitively", () => {
34
40
  expect(route(shippedManifest(), "FOCUS")).toBe("focus-management");
35
41
  });