@roku-sdk/ai 0.0.0-stage → 2.18.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.
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +47 -3
- package/skills/ads-and-raf/SKILL.md +231 -0
- package/skills/ads-and-raf/references/integration-flows.md +332 -0
- package/skills/app-packaging/SKILL.md +107 -0
- package/skills/app-performance/SKILL.md +133 -0
- package/skills/cli-dev-loop/SKILL.md +217 -0
- package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
- package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
- package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
- package/skills/data-entry-keyboards/SKILL.md +160 -0
- package/skills/device-management/SKILL.md +158 -0
- package/skills/dialogs-and-modals/SKILL.md +210 -0
- package/skills/effects/SKILL.md +181 -0
- package/skills/fetch-and-data-loading/SKILL.md +164 -0
- package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
- package/skills/flex-grid/SKILL.md +236 -0
- package/skills/flex-grid/references/focus-integration.md +355 -0
- package/skills/flex-grid/references/plugins.md +201 -0
- package/skills/focus-management/SKILL.md +205 -0
- package/skills/focus-management/references/focus-recipes.md +282 -0
- package/skills/fonts-and-glyphs/SKILL.md +176 -0
- package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
- package/skills/logging-and-errors/SKILL.md +227 -0
- package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
- package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
- package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
- package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
- package/skills/mcp-setup/SKILL.md +131 -0
- package/skills/project-setup/SKILL.md +210 -0
- package/skills/project-setup/references/roku-config.md +120 -0
- package/skills/screen-navigation/SKILL.md +166 -0
- package/skills/sdk-components/SKILL.md +213 -0
- package/skills/sdk-components/references/dialogs.md +148 -0
- package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
- package/skills/sdk-installation/SKILL.md +129 -0
- package/skills/slideshow/SKILL.md +157 -0
- package/skills/slideshow/references/focus-and-handoff.md +187 -0
- package/skills/solid-on-roku/SKILL.md +181 -0
- package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
- package/skills/testing/SKILL.md +147 -0
- package/skills/testing/references/async-and-timing.md +97 -0
- package/skills/user-accounts-and-purchases/SKILL.md +164 -0
- package/skills-manifest.json +316 -0
- package/skills-manifest.schema.json +62 -0
- package/src/__tests__/manifest.test.ts +267 -0
- package/src/__tests__/snippets.test.ts +221 -0
- package/src/__tests__/trigger-routing.test.ts +77 -0
- package/src/index.ts +36 -0
- package/src/manifest.ts +215 -0
- package/src/router.ts +147 -0
- package/src/snippets.ts +212 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# CLI Dev Loop
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This is the build-install-drive-observe loop run from a shell rather than from an agent session:
|
|
6
|
+
the same operations the MCP tools expose, reached through `rk` directly, which is what a script
|
|
7
|
+
or a CI job needs.
|
|
8
|
+
|
|
9
|
+
**The normal way to get a channel onto a device is `rk dev`.** With no flags it builds and
|
|
10
|
+
sideloads a debug build in one step; everything else in this loop is a variation on it. Reach for
|
|
11
|
+
`rk dev` first; do not hand-assemble a package or improvise an install path.
|
|
12
|
+
|
|
13
|
+
Two things about the loop surprise people. The two build commands write to different
|
|
14
|
+
directories and the install command reads only one of them, so the obvious pairing installs the
|
|
15
|
+
wrong thing. And most commands run in process while a few refuse without a server, which turns a
|
|
16
|
+
working local command into a failing CI step.
|
|
17
|
+
|
|
18
|
+
## When to Use
|
|
19
|
+
|
|
20
|
+
- a shell script or CI job that has to build an RSG SDK TypeScript app and get it onto a Roku
|
|
21
|
+
- an install that keeps landing an older build than the one just produced
|
|
22
|
+
- sending remote keys, capturing the screen or reading the node tree with no agent involved
|
|
23
|
+
- collecting what the app printed on the device, from a job nobody is watching
|
|
24
|
+
- turning a crash stack back into TypeScript after the run is over
|
|
25
|
+
- a flag one of the two build commands accepts and the other rejects
|
|
26
|
+
|
|
27
|
+
### When not to use
|
|
28
|
+
|
|
29
|
+
| Situation | Use instead | Because |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| An agent session whose Roku tools already answer | The `mcp-ai-dev-loop` skill | Same loop, different surface: those tools carry their own arguments and their own failure modes |
|
|
32
|
+
| A session with no Roku tools in it at all | The `mcp-setup` skill | Nothing here is the reason an editor lists no tools |
|
|
33
|
+
| What the running app *costs* — dropped frames, slow start, memory | The `app-performance` skill | These commands report what the app did, not what it spent doing it |
|
|
34
|
+
| Registering a Roku, its password, or which one is targeted | The `device-management` skill | Everything here assumes a target already resolves |
|
|
35
|
+
| Signing an app, publishing it, or clearing what is installed | The `app-packaging` skill | Those act on an artifact once this loop has produced one |
|
|
36
|
+
| Writing the app's code, or the project's own configuration | The `sdk-components` and `project-setup` skills | This loop starts once there is something to run |
|
|
37
|
+
|
|
38
|
+
## Read the reference first for these tasks
|
|
39
|
+
|
|
40
|
+
| If the request is | Read now, before answering |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Collecting device logs from a script, or decoding a crash stack outside an agent session | `references/logs-and-crashes.md` |
|
|
43
|
+
|
|
44
|
+
Both are commands whose shell spelling has options the tool surface does not, and `rk logs` is
|
|
45
|
+
one of the few things here that will not run without a server.
|
|
46
|
+
|
|
47
|
+
## BrightScript-only channels
|
|
48
|
+
|
|
49
|
+
A traditional BrightScript/XML app can set `app.brsOnly: true` in
|
|
50
|
+
`roku-config.cjs`. Run `rk build --local` or `rk dev --local`: no tsconfig,
|
|
51
|
+
JavaScript bundle or Hermes bytecode is needed, and stale `ts_path` entries
|
|
52
|
+
are removed. `--no-write-ts-path` alone does not skip bundling. Keep `--local`
|
|
53
|
+
for build/dev until server project discovery supports non-TS configs. Sideload
|
|
54
|
+
and package defaults discover `.ts`, `.cjs`, `.js` and `.mjs`. `entryPoint` is
|
|
55
|
+
optional with `brsOnly`, and `--compile-ts` warns that it is ignored. Pipeline
|
|
56
|
+
plugins anchored to TS build slots are unsupported. Root assets are limited to the native
|
|
57
|
+
channel directories; use `app.staticAssetsDir` for a separate curated channel
|
|
58
|
+
tree. The output/install pairing below is unchanged.
|
|
59
|
+
|
|
60
|
+
## Quick Reference
|
|
61
|
+
|
|
62
|
+
| Goal | Do |
|
|
63
|
+
|---|---|
|
|
64
|
+
| **Put a channel on a device (the normal sideload path)** | **`rk dev` — builds and sideloads in one step** |
|
|
65
|
+
| Production build (artifact only) | `rk build` — writes `dist-build` |
|
|
66
|
+
| Dev build and install together | `rk dev` — writes `dist-dev` |
|
|
67
|
+
| Install something already built | `rk dev --no-build` — reads `<out-dir>/pkg-dir`, defaulting to `dist-dev` |
|
|
68
|
+
| Dev build with no device on the network | `rk dev --no-sideload` — writes `dist-dev`, installs nothing |
|
|
69
|
+
| Press one key | `rk ecp keypress <Key>` |
|
|
70
|
+
| Press a whole path | `rk keysequence Down Down Select` |
|
|
71
|
+
| Capture the screen | `rk ecp capture-screen -o shot.png` |
|
|
72
|
+
| Read the live node tree | `rk ecp query/app-ui --json` |
|
|
73
|
+
| Hand the app launch parameters | `rk ecp input --json '<json>'` |
|
|
74
|
+
| Device logs | `rk logs start`, then `get`, `search` or `tail` — needs a server |
|
|
75
|
+
| A crash stack back to TypeScript | `rk decode-stack` |
|
|
76
|
+
| Fresh-checkout `type-check` / XML JSX types without a build | `rk gen-types` — writes gitignored `.xml.d.ts`; app scripts usually run it before `tsc` |
|
|
77
|
+
| Frames from a Dynamic Component Library left unresolved | add `--dcl-dir <libName>=<dir>` — one per library |
|
|
78
|
+
| Start a stopped cloud device and build while it boots | `rk dev --start --device rce:<name>` — then `rk device stop rce:<name>` |
|
|
79
|
+
|
|
80
|
+
## Instructions
|
|
81
|
+
|
|
82
|
+
1. **The two build commands write to different directories, and a no-build install reads only
|
|
83
|
+
one.** `rk build` writes `dist-build`; `rk dev` writes `dist-dev`; `rk dev --no-build` reads
|
|
84
|
+
`<out-dir>/pkg-dir`, which defaults to `dist-dev`. So `rk build` followed by a bare
|
|
85
|
+
`rk dev --no-build` still installs out of `dist-dev`. It no longer does so silently: when a
|
|
86
|
+
newer `pkg-dir` sits in the other directory, the command says which artifact it is installing,
|
|
87
|
+
which one is newer, and the flag that switches — and when the directory is empty it names a
|
|
88
|
+
command that can actually create it. Pass `--out-dir` matching the build, or let `rk dev` do
|
|
89
|
+
both steps.
|
|
90
|
+
|
|
91
|
+
A project that sets `outDir` in `roku-config.ts` sidesteps this entirely: the install path now
|
|
92
|
+
loads project config the way `rk build` and `rk dev` do, so both halves agree by construction.
|
|
93
|
+
An `outDir` that came from config or a flag is treated as deliberate and draws no warning.
|
|
94
|
+
|
|
95
|
+
2. **One command puts an app on a box: `rk dev`.** With no flags it builds and installs; with
|
|
96
|
+
`--no-build` it installs what already exists. There is no `rk deploy`, no `rk install` and no
|
|
97
|
+
`rk push` — the CLI rejects an unknown command rather than guessing, so a script written
|
|
98
|
+
against a remembered name fails on its first line.
|
|
99
|
+
|
|
100
|
+
Two older spellings still work and print the replacement on stderr: `rk start-dev` is now
|
|
101
|
+
`rk dev`, and the top-level `rk sideload` is now `rk dev --no-build`. Both are deprecated and
|
|
102
|
+
will be removed; `rk sideload`'s own options are unchanged in the meantime.
|
|
103
|
+
|
|
104
|
+
`--no-build` rejects every option that only shapes a build — `--watch`, `--debug`,
|
|
105
|
+
`--inspect`, `--inspect-persist`, `--drop-console`, `--drop-debugger`, `--solid-dev-mode`,
|
|
106
|
+
`--no-write-ts-path`, `--no-typecheck`, `--build-type` and the `--perfetto-*` set —
|
|
107
|
+
rather than accepting and ignoring them. Device targeting (`--device`, `--host`, `--password`,
|
|
108
|
+
`--dap-debug`) and launch options (`--launch-params`, `--no-launch`, `--console`) still apply.
|
|
109
|
+
|
|
110
|
+
`--no-sideload` is the inverse: it builds and stops before the sideload, so it rejects the
|
|
111
|
+
device-facing set instead (`--device`, `--host`, `--password`, `--dap-debug`,
|
|
112
|
+
`--launch-params`, `--no-launch`, `--console`, `--auto-console-close`) and keeps every build
|
|
113
|
+
option, `--watch` included. The two are opposites and cannot be combined.
|
|
114
|
+
|
|
115
|
+
3. **A dev build that needs no Roku at all is `rk dev --no-sideload`.** This is the one worth
|
|
116
|
+
knowing for CI: plain `rk dev` resolves a device *before* it builds and exits non-zero when
|
|
117
|
+
none is active and no password is stored, so a job that only wants to prove the dev bundle
|
|
118
|
+
compiles fails on infrastructure it does not need. `--no-sideload` never reaches that step.
|
|
119
|
+
Output still lands in `dist-dev` unless `--out-dir` moves it, and the artifact is what a
|
|
120
|
+
normal `rk dev` would have installed. For a *production* package it is still `rk build`.
|
|
121
|
+
The other way round, a build that compiles but never reaches the box fails the step: without
|
|
122
|
+
`--watch`, `rk dev` exits 1 with `SIDELOAD_FAILED` when the install failed, and with
|
|
123
|
+
`LAUNCH_FAILED` when the app installed but did not launch — the box then holds the new build,
|
|
124
|
+
and `rk dev --no-build` retries without rebuilding.
|
|
125
|
+
|
|
126
|
+
4. **A few commands need a server; most do not.** `build`, `dev`, `device`, `package`,
|
|
127
|
+
`dcl` and `perfetto` try a running server first and fall back to an in-process
|
|
128
|
+
path, so they work either way; `--local` skips the discovery and is accepted by exactly those
|
|
129
|
+
six. `rk logs` and `rk cdp` have no in-process path and refuse when no server is running,
|
|
130
|
+
which is why a log step that works on a laptop with an editor open fails in CI. Start
|
|
131
|
+
`rk server` in the job first. The one exception is a cloud device's firmware log,
|
|
132
|
+
`rk logs tail --source firmware`, which runs in process.
|
|
133
|
+
|
|
134
|
+
5. **Flags do not carry between the two build commands.** `--metafile` belongs to `rk build`
|
|
135
|
+
alone. `--watch` is not compatible with `--archive`, and `--output`, `--bump-minor` and
|
|
136
|
+
`--bump-build` mean nothing without it. Both `rk build` and `rk dev` accept `--drop-console` and `--drop-debugger`.
|
|
137
|
+
|
|
138
|
+
6. **`--inspect-persist` outlives the run that set it.** Every later launch waits for a debugger
|
|
139
|
+
until the app is reinstalled *without* the flag, so a box left in that state looks hung. It
|
|
140
|
+
also needs firmware 16.0 or later; on anything earlier the field is ignored during sideload
|
|
141
|
+
and nothing persists.
|
|
142
|
+
|
|
143
|
+
7. **`--auto-console-close` is turned off unless `--console` is set.** Nothing warns about it —
|
|
144
|
+
the flag is simply cleared, so on its own it has no effect at all.
|
|
145
|
+
|
|
146
|
+
8. **`ecp input --json` takes a JSON string, not a boolean.** Almost everywhere else in the CLI
|
|
147
|
+
`--json` is a switch that changes the output format; on this one subcommand it carries the
|
|
148
|
+
payload.
|
|
149
|
+
|
|
150
|
+
9. **Key arguments take the wire name, not the runtime one.** `Select` (not `OK`), `Rev` (not
|
|
151
|
+
`rewind`), `Fwd` (not `fastforward`), `Info` (not `options`). The two vocabularies are
|
|
152
|
+
inverted for exactly these four: the name a script passes to `rk ecp` is the enum member,
|
|
153
|
+
while the string the firmware delivers to `onKey` is the other one. A key is validated
|
|
154
|
+
before anything is sent, so a wrong name fails the command outright rather than pressing
|
|
155
|
+
nothing.
|
|
156
|
+
|
|
157
|
+
11. **A Roku Cloud Emulator device is billed while it runs, and a job has to stop what it
|
|
158
|
+
starts.** Nothing in the loop starts a stopped one on its
|
|
159
|
+
own: every command that needs it fails with `RCE_DEVICE_NOT_RUNNING` until `rk device start` or
|
|
160
|
+
`rk dev --start` does. Plain `rk dev --start` leaves the device running when it exits — only
|
|
161
|
+
with `--watch`, or `--console` without `--auto-console-close`, does it stop the device on the
|
|
162
|
+
way out — so end the job with
|
|
163
|
+
`rk device stop`, from a `trap` so a failed step still reaches it. A run this machine did not
|
|
164
|
+
start asks for confirmation before stopping, and refuses without `--yes` when there is no
|
|
165
|
+
terminal to ask on. The run is also capped by its maximum runtime — `--max-runtime` on
|
|
166
|
+
`rk dev --start` or `rk device start`, else the registration's, else an hour — but billing can
|
|
167
|
+
outlast the cap by minutes. In CI give the
|
|
168
|
+
job its token through `RK_RCE_TOKEN`. Interrupted while still holding a device they started,
|
|
169
|
+
`rk dev` and `rk device start` stop it and exit 130 (143 on `SIGTERM`), so a step the runner
|
|
170
|
+
cancelled does not read as an ordinary failure.
|
|
171
|
+
The device and its registration are the `device-management` skill's subject.
|
|
172
|
+
|
|
173
|
+
## Example
|
|
174
|
+
|
|
175
|
+
A CI step that builds, installs the artifact it just built, drives the app to a known screen and
|
|
176
|
+
captures evidence — with the server started because the log step needs one.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
rk server start --daemon
|
|
180
|
+
|
|
181
|
+
rk build --out-dir dist-ci
|
|
182
|
+
rk dev --no-build --out-dir dist-ci --device ci-box
|
|
183
|
+
|
|
184
|
+
rk logs start --device ci-box
|
|
185
|
+
rk keysequence Down Down Select --device ci-box
|
|
186
|
+
rk ecp capture-screen -o evidence.png --device ci-box
|
|
187
|
+
rk logs get --lines 500 --output run.log --device ci-box
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Both `--out-dir` values have to agree. Dropping it from just the install line is the failure in
|
|
191
|
+
instruction 1: it would install from `dist-dev`, which this job never wrote. That case now fails
|
|
192
|
+
with an error naming `dist-ci` as the directory that does hold a `pkg-dir`, rather than
|
|
193
|
+
succeeding against a stale one.
|
|
194
|
+
|
|
195
|
+
## Common Mistakes
|
|
196
|
+
|
|
197
|
+
| Symptom | Cause | Fix |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| The device runs an older build after a green build step | The install read `dist-dev` while `build` wrote `dist-build` | Give both the same `--out-dir`, or use `rk dev`. The install now warns when a newer build sits elsewhere |
|
|
200
|
+
| A script fails on its very first command | It was written against a `deploy` subcommand that does not exist | `rk dev` to build and install, `rk dev --no-build` to install |
|
|
201
|
+
| `[deprecated]` on stderr from a working command | `rk start-dev` and top-level `rk sideload` are aliases now | `rk dev` and `rk dev --no-build` |
|
|
202
|
+
| The log command works locally and refuses in CI | `rk logs` is server-mode only | `rk server start --daemon` earlier in the job |
|
|
203
|
+
| `--metafile` rejected | It belongs to `rk build` | Move the analysis to the production build |
|
|
204
|
+
| `--watch` and `--package` together do nothing sensible | They are not compatible | Watch while developing, package when releasing |
|
|
205
|
+
| A build flag rejected next to `--no-build` | There is no build for it to shape | Drop the flag, or drop `--no-build` to build too |
|
|
206
|
+
| A CI job that only compiles fails on "no active device" | Plain `rk dev` resolves a device before it builds | `rk dev --no-sideload` — it contacts nothing |
|
|
207
|
+
| `--host` or `--console` rejected next to `--no-sideload` | No device is contacted, so they would be ignored | Drop the flag, or drop `--no-sideload` to install too |
|
|
208
|
+
| Every launch hangs waiting for a debugger | `--inspect-persist` is still set on the box | Reinstall without the flag |
|
|
209
|
+
| `--inspect-persist` appears to do nothing | Firmware is older than 16.0 | Nothing persists there; use `--inspect` per run |
|
|
210
|
+
| `--auto-console-close` has no effect | It is cleared unless `--console` is set | Pass `--console` as well |
|
|
211
|
+
| `ecp input --json` rejects a bare `true` | Here the option carries a JSON string | Pass the payload, quoted |
|
|
212
|
+
| The app's own frames decode but a Dynamic Component Library's do not | A DCL is built separately, so its maps are not under the app's build directory | `--dcl-dir <libName>=<dir>`, naming the library — a bare path is rejected |
|
|
213
|
+
| `rk dev` fails with `LAUNCH_FAILED` after a green build | The app installed, but launching it failed; the reason is printed above the error | Fix what the reason names, then `rk dev --no-build` to install and launch the same build without rebuilding |
|
|
214
|
+
| A finished CI job left a cloud device running, and billing | `rk dev --start` stops it only while `--watch`, or `--console` without `--auto-console-close`, keeps it running | `trap 'rk device stop rce:<name> --yes' EXIT` before the start |
|
|
215
|
+
| `rk ecp capture-screen` fails with `SCREENSHOT_NOT_OK` on a cloud or secure retail device | It captures through the installer, which works only while the sideloaded app is in front | Install or relaunch the app, then capture; `Home` breaks it as well |
|
|
216
|
+
| `rk ecp capture-screen` on a LAN device fails with `ECP_ENDPOINT_NOT_FOUND`, naming an auth token | A secure device, every retail Roku among them, serves no ECP capture without a Roku-issued token, and no developer password was at hand for the installer fallback | `rk device update <device> --password <password>` once, then capture again |
|
|
217
|
+
| Every capture after some point in a key sequence shows the wrong thing, or the same thing | `Home` was sent expecting the app's own home screen — the remote's `Home` exits to the Roku home screen, and every capture after it is of that | Move within the app with `Back`; install again to relaunch one that was exited |
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Logs and crashes from a shell
|
|
2
|
+
|
|
3
|
+
What a device printed, and what a crash stack means, reached through `rk` rather than through an
|
|
4
|
+
agent's tools. The reasoning about crashes — which route a stack arrives by, why a bytecode frame
|
|
5
|
+
cannot be read directly, and when exception tagging is needed — belongs to the `mcp-ai-dev-loop`
|
|
6
|
+
skill's `crash-and-console.md` and is not repeated here. This page is the shell surface: which
|
|
7
|
+
command, which option, and what only exists here.
|
|
8
|
+
|
|
9
|
+
## `rk logs` needs a server
|
|
10
|
+
|
|
11
|
+
`rk logs` is server-mode only. Every other command in this loop runs in process, so a log step is
|
|
12
|
+
the one that works on a laptop with an editor open and fails in a CI job that never started a
|
|
13
|
+
server. Start one first:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
rk server start --daemon
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Collection is a session, not a stream you attach to. Nothing is buffered until `start` runs, so a
|
|
20
|
+
job that reproduces a fault and *then* asks for lines gets nothing.
|
|
21
|
+
|
|
22
|
+
| Subcommand | What it is for |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `start` | Begin buffering. `--buffer-size <lines>` sizes the circular buffer |
|
|
25
|
+
| `list` | Sessions that exist. `--verbose` adds metrics |
|
|
26
|
+
| `get` | Pull the last N lines. `--lines`, `--output <file>`, `--append`, `--format <txt\|json>` |
|
|
27
|
+
| `search` | Query the buffer for terms. `--case-sensitive` |
|
|
28
|
+
| `tail` | Follow in real time. `--prefix` stamps each line with the time and the device |
|
|
29
|
+
| `stop` | End one session, or every session with `--all` |
|
|
30
|
+
| `ports` | The log ports the CLI knows about |
|
|
31
|
+
|
|
32
|
+
Nothing enforces which subcommand an option belongs to: one allow-list covers `rk logs` as a
|
|
33
|
+
whole, so `--lines` passed to `start` parses fine and is then ignored rather than rejected.
|
|
34
|
+
`--lines`, `--output`, `--append` and `--format` act on `get`, `--case-sensitive` on `search`,
|
|
35
|
+
`--all` on `stop`, `--prefix` on `tail`, `--buffer-size` on `start` and `--verbose` on `list`.
|
|
36
|
+
`--port` overrides the device's log port, which defaults to 8085.
|
|
37
|
+
|
|
38
|
+
One exception needs no server: on a Roku Cloud Emulator device, `rk logs tail --source firmware
|
|
39
|
+
--device rce:<name>` streams the device's firmware log in process — system output, not the app's
|
|
40
|
+
console, which still comes from a server session. It accepts only `--device` and `--from-start`,
|
|
41
|
+
and rejects anything else rather than ignoring it.
|
|
42
|
+
|
|
43
|
+
Writing to a file is `get --output <file>`, not a shell redirect: `--append` then adds to it
|
|
44
|
+
across several calls in the same job instead of truncating.
|
|
45
|
+
|
|
46
|
+
## `rk decode-stack`, and what the flags are called
|
|
47
|
+
|
|
48
|
+
The same decode the tool surface performs, with the parameters spelled as CLI options:
|
|
49
|
+
|
|
50
|
+
| Tool parameter | CLI option |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `stackTrace` | the input — `--file <path>`, or piped in with `--stream` |
|
|
53
|
+
| `buildDir` | `--build-dir`, `-b` |
|
|
54
|
+
| `sha` | `--sha` |
|
|
55
|
+
| `context` | `--context`, `-c` |
|
|
56
|
+
| `dclDirs` | `--dcl-dir` |
|
|
57
|
+
|
|
58
|
+
The input has two shell spellings the tool parameter does not, both standing in for `stackTrace`:
|
|
59
|
+
|
|
60
|
+
- `--stream` reads the trace from standard input line by line, so a live log can be piped straight
|
|
61
|
+
in without landing it somewhere first.
|
|
62
|
+
- `--file <path>` reads it from a file, which is the pairing for `rk logs get --output`.
|
|
63
|
+
|
|
64
|
+
Three options have no counterpart in the tool surface at all, and they are what make the command
|
|
65
|
+
usable from a script rather than a session:
|
|
66
|
+
|
|
67
|
+
- `--output <path>`, `-o` writes the decoded result to a file instead of the terminal. It also
|
|
68
|
+
switches the format to `plain` unless `--format` or `--json` was passed too, so a job that wants
|
|
69
|
+
JSON in the file has to ask for it explicitly.
|
|
70
|
+
- `--format` and `--json` choose the shape of that result, so a job can parse it.
|
|
71
|
+
- `--timeout`, `-t` bounds the wait for a trace on standard input and nothing else — it defaults
|
|
72
|
+
to 30s, and with `--file` or `--stream` it is a no-op that warns on stderr. It is not a cap on
|
|
73
|
+
how long the decode itself may take.
|
|
74
|
+
|
|
75
|
+
Two options validate their input rather than accepting anything:
|
|
76
|
+
|
|
77
|
+
- `--sha` must be a hexadecimal string of at least 8 characters. A shorter or non-hex value is
|
|
78
|
+
rejected outright, which is the good case — see the warning below for the bad one.
|
|
79
|
+
- `--context` must be a non-negative integer no greater than 50.
|
|
80
|
+
|
|
81
|
+
`--dcl-dir` is the option to reach for when frames name a Dynamic Component Library instead of
|
|
82
|
+
`index`: it points the decoder at that library's own build output, which its separate build left
|
|
83
|
+
outside the app's. Its value is not a bare path but `libName=/path/to/dir`, and a value with no
|
|
84
|
+
`=` in it is rejected saying so; repeat the option once per library. A library whose frames stay
|
|
85
|
+
unresolved after the option is passed is usually a `libName` that does not match the one the frames
|
|
86
|
+
name, rather than a wrong path.
|
|
87
|
+
|
|
88
|
+
## The failure that looks like success
|
|
89
|
+
|
|
90
|
+
A `--sha` naming a build directory that is not there does not fail. The decoder falls back to
|
|
91
|
+
detecting a build automatically and resolves against whichever one is newest on disk, so the
|
|
92
|
+
result is confident, complete, and about the wrong code. In a CI job that rebuilds between the
|
|
93
|
+
crash and the decode, this is the likely outcome rather than an unlikely one.
|
|
94
|
+
|
|
95
|
+
Two habits avoid it: decode before rebuilding, and treat a decoded frame that does not match the
|
|
96
|
+
code you believe ran as a suspect `--sha` before suspecting the frame.
|
|
97
|
+
|
|
98
|
+
## The pairing a job actually uses
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
rk server start --daemon
|
|
102
|
+
rk logs start --device ci-box
|
|
103
|
+
|
|
104
|
+
# ... run whatever reproduces the fault ...
|
|
105
|
+
|
|
106
|
+
rk logs get --lines 2000 --output device.log --device ci-box
|
|
107
|
+
rk logs stop --all
|
|
108
|
+
rk decode-stack --file device.log --build-dir dist-ci/bundle --output decoded.txt
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--build-dir` is not optional in a monorepo. `decode-stack` never goes through the server, so both
|
|
112
|
+
the automatic build detection and a relative `--build-dir` are anchored at the directory the
|
|
113
|
+
command is run from — a job that runs it from the repo root while the app builds into a
|
|
114
|
+
subdirectory reports no source map for every frame until it is given.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# CPU and Heap Profiling
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The server's CDP profiler answers "which JavaScript function is burning the time" and "what is
|
|
6
|
+
holding memory" against the running app, and writes the result to a file you then read. It rides
|
|
7
|
+
on a debug session, so the loop is fixed: create the session, drive the app to the state worth
|
|
8
|
+
measuring, start a capture, drive the interaction, stop it, read the file. A CPU capture only
|
|
9
|
+
names functions on an unminified build, which is the precondition to settle before recording.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- a screen, key press or list that is slow and the question is which TypeScript function is hot
|
|
14
|
+
- JS memory that grows and the question is what is retaining it
|
|
15
|
+
- comparing a `.cpuprofile`, `.heapprofile` or `.heapsnapshot` before and after a change
|
|
16
|
+
- profiling while also evaluating expressions in the same app (`cdp_eval`)
|
|
17
|
+
|
|
18
|
+
### When not to use
|
|
19
|
+
|
|
20
|
+
| Situation | Use instead | Because |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Frames dropped, a transition janks, the render thread is the suspect | The `app-performance` skill | A Perfetto trace shows the render and transaction threads; a CPU profile only sees JavaScript |
|
|
23
|
+
| Only total and used heap bytes are wanted, or whether they climb the longer the app runs | `cdp_heap_usage` in `app-performance` | A capture is heavier than the question; read the size first, and capture only once the question becomes what holds it |
|
|
24
|
+
| The app crashed and there is a stack | The `mcp-ai-dev-loop` skill | Crashes are decoded, not profiled |
|
|
25
|
+
| No profiling tools in the session | The `mcp-setup` skill | These tools exist only against a running `rk server` |
|
|
26
|
+
| No agent session, a shell script or CI job | Nothing here drives it from a shell | The tools are MCP and REST; the loop below is what a script would have to reproduce |
|
|
27
|
+
|
|
28
|
+
## Quick Reference
|
|
29
|
+
|
|
30
|
+
| Question | Call | Gives |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Open the session | `cdp_session_create { root }` | Builds and sideloads a debug build, connects |
|
|
33
|
+
| Which functions are hot | `cdp_profile_start`, then `cdp_profile_stop` | A `.cpuprofile` file |
|
|
34
|
+
| Profile the next N ms in one call | `cdp_profile_start { durationMs }` | The server stops and writes it; no stop call |
|
|
35
|
+
| What retains memory right now | `cdp_heap_snapshot` | A `.heapsnapshot`, blocks until written |
|
|
36
|
+
| Which call sites hold allocations | `cdp_heap_sampling_start`, then `cdp_heap_sampling_stop` | A `.heapprofile` |
|
|
37
|
+
| Release the device | `cdp_session_end` | One session per device |
|
|
38
|
+
|
|
39
|
+
Every response is metadata (`filePath`, byte and node counts, a `captureId`), never the profile.
|
|
40
|
+
Read the file at `filePath`. Steps 4 and 5 below, and the shape of each file, are in
|
|
41
|
+
`references/reading-profiles.md`: read it before summarising a profile, because the two
|
|
42
|
+
mistakes that make one wrong — reading self time as total time, and reading a minified
|
|
43
|
+
build — both look like a finished answer.
|
|
44
|
+
|
|
45
|
+
## Instructions
|
|
46
|
+
|
|
47
|
+
1. **Profile a debug build.** `cdp_session_create` builds in debug mode, which is unminified
|
|
48
|
+
JavaScript, so frames keep their function names; no config change is needed. From a terminal
|
|
49
|
+
the same build is `rk dev --debug`. If every frame is a short renamed symbol, the profile came
|
|
50
|
+
from a minified build (`rk dev --release`, or `--minify`): rebuild with `--debug`. Profile
|
|
51
|
+
timings from a debug build have no Hermes bytecode: use them to rank functions against each
|
|
52
|
+
other, not to quote milliseconds.
|
|
53
|
+
2. **Create the session.** `cdp_session_create { root }` with the absolute app root. It needs a
|
|
54
|
+
running `rk server`. A second session for the same device is refused until `cdp_session_end`.
|
|
55
|
+
3. **Drive the app to the starting state before recording.** Use `ecp`, `capture_screen`, or
|
|
56
|
+
`cdp_eval` to reach the screen, so the capture holds the interaction and not the navigation to it.
|
|
57
|
+
4. **Start, drive, stop.** `cdp_profile_start`, perform the interaction, `cdp_profile_stop`.
|
|
58
|
+
Keep the window to the interaction; a long capture buries it. For "the next N ms", pass
|
|
59
|
+
`durationMs` and skip the stop call: the file appears at the `filePath` the start returned,
|
|
60
|
+
and a later `cdp_profile_stop` answers `CAPTURE_NOT_ACTIVE`.
|
|
61
|
+
5. **Read the file, then answer.** Open `filePath`, rank functions by self time, and map the
|
|
62
|
+
top ones back to source with `cdp_source_grep` or `cdp_source_get`.
|
|
63
|
+
6. **Change one thing, capture again.** One profile is not evidence: the sampler is a few
|
|
64
|
+
hundred hertz at best, so a function that shows once may be noise. Repeat the same
|
|
65
|
+
interaction and compare what repeats.
|
|
66
|
+
7. **End the session and redeploy.** `cdp_session_end`, then redeploy normally so the next
|
|
67
|
+
person does not measure your debug build.
|
|
68
|
+
|
|
69
|
+
### Profiling and evaluating in one session
|
|
70
|
+
|
|
71
|
+
`cdp_eval`, `cdp_profile_*` and `cdp_heap_*` interleave freely on one session: eval, profile,
|
|
72
|
+
eval again needs no reconnect. The exception is any `cdp_debugger_*` tool, including arming a
|
|
73
|
+
breakpoint: after the first one, `cdp_eval` is disabled for that connection for good, while the
|
|
74
|
+
profiler tools keep working. So a session that already ran a debugger tool cannot combine
|
|
75
|
+
profiling with evaluation. `cdp_session_end`, then `cdp_session_create` a fresh one, and do
|
|
76
|
+
the eval and profile work in it without touching a debugger tool.
|
|
77
|
+
|
|
78
|
+
## Example
|
|
79
|
+
|
|
80
|
+
Profiling a slow screen transition, then checking the heap:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
# cdp_session_create builds a debug (unminified) build; no config change needed
|
|
84
|
+
cdp_session_create { root } -> sessionId
|
|
85
|
+
ecp { keypress: Select } -- reach the screen
|
|
86
|
+
cdp_profile_start { samplingHz: 200 } -> captureId, filePath
|
|
87
|
+
ecp { keypress: Right } -- the slow transition
|
|
88
|
+
cdp_profile_stop {} -> filePath, nodeCount, sampleCount, durationMs
|
|
89
|
+
cdp_heap_snapshot {} -> filePath, bytes, nodeCount, edgeCount
|
|
90
|
+
cdp_session_end {}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Common Mistakes
|
|
94
|
+
|
|
95
|
+
| Symptom | Cause | Fix |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| Frames are single letters or `(anonymous)` | The build was minified (`--release` or `--minify`) | Rebuild with `--debug` (or a fresh `cdp_session_create`), capture again |
|
|
98
|
+
| `NO_CDP_SESSION` | Session missing, or created with `rk cdp` rather than `cdp_session_create` | `cdp_session_create`; the aliased tools cannot see a REST-created one |
|
|
99
|
+
| `CAPTURE_ALREADY_ACTIVE` / `SAMPLING_ALREADY_ACTIVE` | A capture is open on this session | Stop it, or pass its `captureId` to the stop call |
|
|
100
|
+
| `CAPTURE_NOT_ACTIVE` on stop | `durationMs` already stopped it | Read the file the start returned |
|
|
101
|
+
| `cdp_eval` refused after profiling worked | A `cdp_debugger_*` tool ran on this session | Fresh session, above |
|
|
102
|
+
| `samplingHz` made no difference | Older firmware may ignore it and sample at its own rate | Compare captures at the same rate; do not tune it |
|
|
103
|
+
| Heap sampling profile is nearly empty | It shows only allocations still live at stop; the collector ran | Stop earlier, or use `cdp_heap_snapshot` for retained memory |
|
|
104
|
+
| `SNAPSHOT_TIMEOUT` | A large heap took over two minutes | Retry once idle; an immediate retry can come back incomplete |
|
|
105
|
+
| Timings look worse than users report | Debug build, no bytecode | Rank, don't quote; confirm with a normal build's trace |
|
|
106
|
+
|
|
107
|
+
The tool descriptions from the server's `tools/list` carry each parameter and response field;
|
|
108
|
+
enumerate them rather than trusting this table.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Reading a profile file
|
|
2
|
+
|
|
3
|
+
The tools write Chrome DevTools formats verbatim, so each file also opens in Chrome DevTools
|
|
4
|
+
(Performance for `.cpuprofile`, Memory for `.heapprofile` and `.heapsnapshot`). An agent reads
|
|
5
|
+
them as JSON.
|
|
6
|
+
|
|
7
|
+
## CPU: `.cpuprofile`
|
|
8
|
+
|
|
9
|
+
`nodes[]` is a call tree: each node has `id`, `callFrame` (`functionName`, `url`, `lineNumber`,
|
|
10
|
+
`columnNumber`) and `children` (ids). `samples[]` is the node id hit at each sample and
|
|
11
|
+
`timeDeltas[]` the microseconds since the previous one.
|
|
12
|
+
|
|
13
|
+
- **Self time** is the sum of `timeDeltas` for samples landing on a node. Rank on it to find
|
|
14
|
+
what burns the time itself.
|
|
15
|
+
- **Total time** is self plus all descendants. A framework wrapper has large total and tiny
|
|
16
|
+
self; reading it as the culprit is the usual wrong answer. Descend to the child with the
|
|
17
|
+
self time.
|
|
18
|
+
- Line and column refer to the generated bundle. Take `functionName`, then find it with
|
|
19
|
+
`cdp_source_grep`.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// node top-self.js file.cpuprofile
|
|
23
|
+
const p = JSON.parse(require("fs").readFileSync(process.argv[2], "utf8"));
|
|
24
|
+
const self = new Map();
|
|
25
|
+
p.samples.forEach((id, i) => self.set(id, (self.get(id) ?? 0) + (p.timeDeltas[i] ?? 0)));
|
|
26
|
+
const byId = new Map(p.nodes.map(n => [n.id, n]));
|
|
27
|
+
const total = [...self.values()].reduce((a, b) => a + b, 0);
|
|
28
|
+
[...self].sort((a, b) => b[1] - a[1]).slice(0, 15).forEach(([id, us]) => {
|
|
29
|
+
const f = byId.get(id).callFrame;
|
|
30
|
+
console.log(`${((us / total) * 100).toFixed(1)}%`, f.functionName || "(anonymous)", `${f.url}:${f.lineNumber}`);
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`(idle)`, `(program)` and `(garbage collector)` rows are the runtime, not app code: a high
|
|
35
|
+
`(idle)` share means the capture window was mostly waiting, not that the app is fast.
|
|
36
|
+
|
|
37
|
+
## Heap sampling: `.heapprofile`
|
|
38
|
+
|
|
39
|
+
`head` is a tree of `callFrame`, `selfSize` (bytes) and `children`. Sum `selfSize` down each
|
|
40
|
+
branch to rank call sites by bytes still live at the stop call. Zero for a site means its
|
|
41
|
+
objects were collected, not that it never allocated.
|
|
42
|
+
|
|
43
|
+
## Heap snapshot: `.heapsnapshot`
|
|
44
|
+
|
|
45
|
+
A large graph (`nodes`, `edges`, `strings` arrays). Do not read it whole. Load it in Chrome
|
|
46
|
+
DevTools' Memory panel and use the Comparison view between two snapshots taken around the
|
|
47
|
+
suspect interaction; growth in a class or closure count is the finding.
|
|
48
|
+
|
|
49
|
+
## Comparing two runs
|
|
50
|
+
|
|
51
|
+
Same interaction, same session state, same `samplingHz`. Compare self-time shares, not
|
|
52
|
+
absolute microseconds: sampling rate and the debug build shift the totals but not the ranking.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# On-screen keyboards and PIN entry
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Five components collect typing from a remote: `DynamicKeyboard` and `DynamicMiniKeyboard` for
|
|
6
|
+
prose, `DynamicPinPad` for a code, `CustomKeyboard` for an arrangement you define, and
|
|
7
|
+
`DynamicKeyGrid` when the keys are choices rather than characters.
|
|
8
|
+
|
|
9
|
+
Two things decide how one is wired. The `Dynamic*` family reports the **whole string so far**
|
|
10
|
+
through `onText`, so app state follows the field rather than accumulating keys. And a
|
|
11
|
+
`CustomKeyboard` reports **which key** was pressed, which is what makes shift and
|
|
12
|
+
language keys the app's job rather than the component's.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
| If the request is | Then |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Reading what a viewer has typed, as they type it | Here — `onText` |
|
|
19
|
+
| Hiding the characters of a code while still showing its length | Here — `secureMode` |
|
|
20
|
+
| An arrangement of keys the defaults do not cover | Here — a key definition file |
|
|
21
|
+
| Whether the SDK already ships the layout you were about to author | Here — the packaged KDFs |
|
|
22
|
+
| Shift or language keys that appear to do nothing | Here — the app drives the mode |
|
|
23
|
+
| Suiting the arrangement to an email address or a URL | Here — the domain |
|
|
24
|
+
|
|
25
|
+
### When not to use
|
|
26
|
+
|
|
27
|
+
| Situation | Use instead | Because |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Which pre-assembled dialog wraps a field, or which item type goes in a composed one | The `sdk-components` skill, and the dialog reference it declares | That skill owns which component and how a dialog is composed. This one owns the entry itself, wherever it sits |
|
|
30
|
+
| Opening and closing the dialog the field sits in, or chaining a step after it | The `dialogs-and-modals` skill | Lifecycle is above the field. This skill has nothing to say about when the dialog is up |
|
|
31
|
+
| Moving focus between the keyboard and the rest of the screen | The `focus-management` skill | Handing focus between regions is its subject. A keyboard is just one region |
|
|
32
|
+
| How to spell these exports in JSX, or why they keep their backing asset | The `sdk-components` skill | It owns the spelling table and the tree-shaking rule for them |
|
|
33
|
+
|
|
34
|
+
## Quick Reference
|
|
35
|
+
|
|
36
|
+
| Component | Collects | Reports through |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `DynamicKeyboard` | Prose, full-width | `onText` |
|
|
39
|
+
| `DynamicMiniKeyboard` | Prose, compact | `onText` |
|
|
40
|
+
| `DynamicPinPad` | Digits | `onText` |
|
|
41
|
+
| `CustomKeyboard` | Whatever the layout defines | `onKeySelectedValue` |
|
|
42
|
+
| `DynamicKeyGrid` | A choice among keys | `onKeySelected` |
|
|
43
|
+
|
|
44
|
+
Props worth knowing before reaching for the source:
|
|
45
|
+
|
|
46
|
+
| Prop | On | Does |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `onText` | the `Dynamic*` family | The full string so far, on every change |
|
|
49
|
+
| `secureMode` | `DynamicKeyboard`, `DynamicPinPad` | Masks the characters; length stays visible |
|
|
50
|
+
| `hintText` | all | Guidance shown while the field is empty |
|
|
51
|
+
| `maxTextLength` | all | Caps the entry |
|
|
52
|
+
| `showUnderscores`, `centerUnderscores` | `DynamicPinPad` | How the unfilled positions are drawn |
|
|
53
|
+
| `domain` | `DynamicKeyboard`, `DynamicMiniKeyboard`, `CustomKeyboard` | Suits the arrangement to a kind of value; typed by `KeyboardDomain` |
|
|
54
|
+
| `keyDefinitionUri` | `CustomKeyboard`, `DynamicKeyGrid` | The layout to use |
|
|
55
|
+
| `mode` | `CustomKeyboard`, `DynamicKeyGrid` | Which arrangement inside that layout is showing |
|
|
56
|
+
| `jumpToKey` | `CustomKeyboard` | Moves the highlight to a key by position |
|
|
57
|
+
| `horizWrap`, `vertWrap` | `DynamicKeyGrid` | Whether movement wraps at an edge |
|
|
58
|
+
|
|
59
|
+
## Instructions
|
|
60
|
+
|
|
61
|
+
1. **Take the value from `onText`, not by accumulating presses.** It hands over the whole
|
|
62
|
+
string each time it changes, so the signal behind the field is a mirror of the field rather
|
|
63
|
+
than a log of keys. Appending on every call is how a field ends up with doubled characters.
|
|
64
|
+
|
|
65
|
+
2. **Mask with `secureMode`.** It hides the characters and leaves the length visible, which is
|
|
66
|
+
what a viewer needs to see that a press registered. Do not build masking yourself by
|
|
67
|
+
holding the real value beside a row of asterisks.
|
|
68
|
+
|
|
69
|
+
3. **Before authoring a layout, check whether one ships.** `@roku-sdk/components/kdfs` exports
|
|
70
|
+
packaged key definitions — an address layout, a MOP PIN pad, a phone pad and a Wi-Fi
|
|
71
|
+
layout — as URIs ready for `keyDefinitionUri`. **Import only the ones you use**: an unused
|
|
72
|
+
export is what keeps the rest of the files out of the build.
|
|
73
|
+
|
|
74
|
+
4. **Author your own as a `.keyboard.json` and import it.** The build rewrites that import to
|
|
75
|
+
the packaged URI, so what you pass to `keyDefinitionUri` is the imported value rather than a
|
|
76
|
+
path you construct. Declaring several arrangements in one file is what makes shift and
|
|
77
|
+
alphabet switching possible at all.
|
|
78
|
+
|
|
79
|
+
5. **Drive `mode` yourself in response to `onKeySelectedValue`.** This is the step that is
|
|
80
|
+
easy to miss: a shift or language key is **suppressed by the component** so it is not typed
|
|
81
|
+
into the field, and nothing switches the arrangement on your behalf. Watch for those keys in
|
|
82
|
+
the handler and set the `mode` signal — the component renders whichever arrangement `mode`
|
|
83
|
+
names.
|
|
84
|
+
|
|
85
|
+
6. **Set `domain` when the value has a shape.** It suits the arrangement to what is being
|
|
86
|
+
collected rather than making the viewer hunt for `@` on a prose layout. The choices are
|
|
87
|
+
typed by `KeyboardDomain`.
|
|
88
|
+
|
|
89
|
+
7. **Let `focus-management` put focus on the keyboard.** These components take key presses once
|
|
90
|
+
focused; getting focus to them, and back out afterwards, is not their concern.
|
|
91
|
+
|
|
92
|
+
## Example
|
|
93
|
+
|
|
94
|
+
A PIN pad that masks its entry, and a custom keyboard whose shift key the app has to honour.
|
|
95
|
+
|
|
96
|
+
```tsx rsg-skill=complete
|
|
97
|
+
import { CustomKeyboard, DynamicPinPad, IKeyHandlerData } from "@roku-sdk/components";
|
|
98
|
+
import { Component, createSignal } from "@roku-sdk/solid";
|
|
99
|
+
|
|
100
|
+
export const ParentalCode: Component<{ onComplete: (code: string) => void }> = props => {
|
|
101
|
+
const [code, setCode] = createSignal("");
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<DynamicPinPad
|
|
105
|
+
secureMode
|
|
106
|
+
maxTextLength={4}
|
|
107
|
+
hintText="Enter your 4-digit code"
|
|
108
|
+
text={code()}
|
|
109
|
+
onText={value => {
|
|
110
|
+
// The whole string so far — assign it, never append to it.
|
|
111
|
+
setCode(value);
|
|
112
|
+
if (value.length === 4) {
|
|
113
|
+
props.onComplete(value);
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const LOWER = "lower";
|
|
121
|
+
const UPPER = "upper";
|
|
122
|
+
|
|
123
|
+
export const NameField: Component<{ layoutUri: string }> = props => {
|
|
124
|
+
const [mode, setMode] = createSignal(LOWER);
|
|
125
|
+
const [text, setText] = createSignal("");
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<CustomKeyboard
|
|
129
|
+
keyDefinitionUri={props.layoutUri}
|
|
130
|
+
mode={mode()}
|
|
131
|
+
text={text()}
|
|
132
|
+
onText={value => {
|
|
133
|
+
setText(value);
|
|
134
|
+
}}
|
|
135
|
+
// A shift key is suppressed rather than typed, so the arrangement
|
|
136
|
+
// only changes if this handler changes it.
|
|
137
|
+
onKeySelectedValue={(data: IKeyHandlerData) => {
|
|
138
|
+
if (String(data.key).toLowerCase().startsWith("caps")) {
|
|
139
|
+
setMode(current => (current === LOWER ? UPPER : LOWER));
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Common Mistakes
|
|
148
|
+
|
|
149
|
+
| Symptom | Cause | Fix |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| Characters appear doubled or the field grows too fast | `onText` was treated as one key and appended to state | It is the whole string; assign it |
|
|
152
|
+
| A code is readable over the viewer's shoulder | Masking was not turned on | `secureMode` |
|
|
153
|
+
| A viewer cannot tell whether a press registered on a masked field | Masking hid the length too, because it was hand-built | `secureMode` keeps the length visible |
|
|
154
|
+
| A shift or language key does nothing, and is not typed either | The component suppresses it; nothing switched the arrangement | Watch for it in `onKeySelectedValue` and set `mode` |
|
|
155
|
+
| A hand-written layout URI does not resolve | A path was constructed rather than using the value the import produced | Import the `.keyboard.json` and pass what it yields |
|
|
156
|
+
| The build carries layouts the app never shows | Every packaged KDF was imported, or a barrel pulled them in | Import only the ones used from `@roku-sdk/components/kdfs` |
|
|
157
|
+
| An email field puts the viewer on a prose arrangement | `domain` was left unset | Set it from `KeyboardDomain` |
|
|
158
|
+
|
|
159
|
+
Props are documented with TSDoc in `@roku-sdk/components`; read a field's exact shape and the
|
|
160
|
+
full `KeyboardDomain` list there rather than inferring either from here.
|