Skip to content

feat(tv): Apple TV (tvOS) and Android TV support#374

Open
scianek wants to merge 33 commits into
mainfrom
feat/tv-support
Open

feat(tv): Apple TV (tvOS) and Android TV support#374
scianek wants to merge 33 commits into
mainfrom
feat/tv-support

Conversation

@scianek

@scianek scianek commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Adds end-to-end support for driving Apple TV (tvOS) and Android TV (leanback) targets through argent, using a focus-driven interaction model (TVs have no touchscreen — interaction is moving a focus highlight with the remote).

What this delivers

Four tv-* tools that dispatch to the right backend automatically by target id:

  • tv-describe — render focused + focusable elements as text
  • tv-navigate — send a remote direction / select / menu / home / playpause
  • tv-set-focus — jump focus to an element by accessibility label
  • tv-type — type into the focused field

Apple TV runs two native daemons (in-sim AX service + host-side HID daemon), shipped via the argent-private submodule. Android TV reuses the same tool surface but is adb-backed (input keyevent, uiautomator dump, input text).

Key design points

  • tvOS UDIDs are UUID-shaped and indistinguishable from iOS by shape. Handled by adding runtimeKind detection (tvOS runtime string for Apple TV; pm list features leanback/television for Android TV — not ro.build.characteristics, which lies on TV emulators).
  • Converted launch-app / restart-app / screenshot / screenshot-diff / run-sequence from eager to lazy service resolution, so a tvOS target never spins up (and hangs on) the iOS-only simulator-server / native-devtools blueprints.
  • Native injection now selects the platform-matched (TVOSSIMULATOR) dylib slice; daemons recycle across sim reboots and app relaunches so injection and focus survive.
  • Two new skills (argent-tv-setup, argent-tv-interact) plus routing updates point agents at the TV tools.

Verification

  • npm run build, npm run lint, and the full tool-server suite (1345 tests) all pass.
  • Core flows verified live against real apps on both an Apple TV 4K simulator and a Google ATV emulator.

Dependency / merge order

Pins the argent-private submodule to the tip of its feat/tv-support branch (software-mansion/argent-private#20). Merge that PR to main first, then bump the submodule pointer here to the merged SHA before merging this PR.

🤖 Generated with Claude Code

scianek added 30 commits June 10, 2026 15:41
Adds the native build + packaging pipeline for the two tvOS daemons whose
ObjC sources land via the argent-private submodule bump (27dac19 → 0feb091):

- tvos-ax-service: compiled against the appletvsimulator SDK
  (arm64-apple-tvos17.0-simulator); `simctl spawn`d into the simulator to
  read the focus-engine accessibility tree.
- tvos-hid-daemon: a host macOS binary (AppKit) that injects Siri-remote
  HID events into the simulator via SimulatorKit.

build.sh compiles both for the unix transport only (no TCP variant) and
codesigns them in release mode. index.ts exposes darwin-only path resolvers
(tvosAxServiceBinaryPath / tvosHidDaemonBinaryPath), and bundle-tools.cjs
copies both into bin/darwin/ so the published package ships them.
listIosSimulators now accepts tvOS runtimes alongside iOS instead of
filtering them out, and tags each simulator with runtimeKind ("mobile" |
"tv") derived from the runtime string.

This is the signal the rest of the tvOS support keys off: blueprints use
the IosSimulator runtimeKind to route an Apple TV udid to the tv-control
service (and to reject it from the iOS-only ax-service), and list-devices
surfaces it so callers can pick the right target.
Introduces the focus-driven interaction surface for tvOS simulators, since
Apple TV has no touch — interaction is moving a focus highlight with the
Siri remote.

tvControlBlueprint manages two daemons per device and exposes a rich
TvControlApi (describe / hierarchy / setFocus / navigate / type / ping) that
hides the wire protocol — each command is one short-lived client connection
to the daemon's unix socket. The factory verifies via simctl that the target
is actually a booted tvOS simulator before spawning anything, since
resolveDevice classifies by udid shape alone and can't tell tvOS from iOS.

Four tools, registered in setup-registry, drive it:
- tv-describe: render focused + focusable elements as text (alwaysLoad)
- tv-navigate: send a Siri-remote direction / select / menu / home / playpause
- tv-set-focus: jump focus to an element by accessibility label
- tv-type: type text into the focused field via HID keyboard

ax-service now rejects tvOS devices with a clear pointer to tvControlBlueprint
instead of trying to spawn the iOS ax-service, and tolerates the tvOS AX
response shape (`focusable` key) in describe().
list-devices.test asserted tvOS simulators were filtered out, but device
discovery now includes them (tagged runtimeKind "tv"). Updated to expect the
Apple TV in the iOS-platform list and to assert the runtimeKind tagging.
The simulator-server backend can't drive tvOS, so `screenshot` failed for
Apple TV targets. A tvOS udid also classifies as iOS by shape, so eagerly
declaring the simulator-server service would spawn it for the tvOS device and
hang on the 30s ready timeout before execute() could branch.

Fix: convert screenshot to the registry-closure form (createScreenshotTool)
with no eagerly-declared service. execute() resolves the backend lazily:

- iOS udid whose runtime is tvOS → capture via `xcrun simctl io <udid>
  screenshot`, downscale the 4K frame with `sips` to honour the same default
  scale as the other platforms (best-effort; falls back to full-res), and
  register the PNG as an artifact.
- everything else → resolve simulator-server on demand and use the existing
  HTTP screenshot path, unchanged.

Verified end-to-end against a booted Apple TV 4K simulator: 3840x2160 capture
downscaled to 1152x648 at scale 0.3, returned as an artifact handle with no
simulator-server spawn.

This is a temporary bridge until tv-control grows a first-class screenshot
capability over its existing daemon transport.
describe dispatches a tvOS udid through the iOS branch (tvOS classifies as
platform "ios" by udid shape), where describeIos tried to spawn the iOS
ax-service inside the Apple TV sim. That spawn can't read the tvOS focus
engine: it timed out on the daemon connection (~10s) and the unconditional
catch degraded with the generic boot-device hint — telling the user to
force-reboot a perfectly healthy simulator.

Fix: short-circuit tvOS at the top of describeIos and return a hint that
points at the real tooling (tv-describe / tv-navigate / tv-set-focus /
tv-type and the argent-tvos-interact skill) instead of ax-service. Now ~140ms
and correct, vs ~10s and misleading.

The tvOS/iOS distinction needs a `simctl list` probe (resolveDevice classifies
by shape alone and leaves runtimeKind undefined), so factor that into a shared
getSimulatorRuntimeKind / isTvOsSimulator in ios-devices, memoized per-udid
since a sim's runtime kind is fixed at creation — this keeps the hot iOS
describe/screenshot path from paying the probe cost on every call. screenshot's
previously-inlined probe now uses the same helper.

Also adds a describe-tool test asserting tvOS returns the tv-describe hint,
never resolves the iOS ax-service, and doesn't emit the boot-device hint.

Note on the auto-screenshot gap (launch-app etc. on tvOS): that path calls the
screenshot tool over HTTP, which already branches to xcrun for tvOS via the
same helper, so it now produces a frame instead of being silently dropped.
Two skills following the existing iOS/Android split:

- argent-tvos-simulator-setup: find/boot an Apple TV simulator (runtimeKind
  "tv") and connect.
- argent-tvos-interact: the focus-driven interaction model (describe → move
  focus → confirm → activate), a tool-selection table, per-tool usage, common
  workflows, and troubleshooting.

Both mirror into packages/argent/skills/ at pack time (that copy is
gitignored); packages/skills/ is the source of truth.
run-sequence couldn't drive Apple TV: the tvOS tools weren't in its
allow-list, and — worse — it eagerly declared the simulator-server service,
which a tvOS udid (iOS by shape) can't drive, so the registry would spawn it
and hang on the ready timeout before any step ran.

- Add tv-navigate / tv-set-focus / tv-type to ALLOWED_TOOLS; the gesture-*
  tools stay iOS/Android-only since tvOS is focus-driven with no coordinates.
- Drop the eager simulator-server declaration. execute() never used it —
  each step already resolves its own services via registry.invokeTool — so
  run-sequence needs no service of its own, and a tvOS sequence no longer
  trips the simulator-server spawn.
- Document the tvOS tools and an example in the tool description and the
  argent-tvos-interact skill (with the caveat to fall back to individual
  tv-describe-gated calls when a step depends on where focus landed).

Verified end-to-end against a booted Apple TV 4K simulator: a two-step
tv-navigate sequence completes 2/2 in ~740ms with no simulator-server spawn.
Adds a run-sequence test covering tvOS dispatch, allow-list rejection, and the
empty services() declaration.
Agents working a tvOS flow were reaching for the iOS skills, since tvOS
UDIDs are UUID-shaped and indistinguishable from iOS by shape alone. The
new tvOS skills existed but nothing pointed to them.

- Add tvOS setup + interaction entries to the <skill_routing> table in
  rules/argent.md (the dispatch table every other skill is listed in), and
  note on the device-interact entry that it's iOS/Android only.
- Add a callout to argent-device-interact's 'Unified tool surface' section:
  a runtimeKind 'tv' target is focus-driven and must use argent-tvos-interact,
  not the gesture/button/keyboard tools.
tv-set-focus always returned "Element not found" because the native
setfocus path required an exact full-string match against tvOS AX
labels, which are compound multi-line strings, and the agent could only
ever supply the label text shown by tv-describe.

- Bump argent-private to pick up normalized first-line matching (with
  prefix/substring fallback) in tvos_ax_service.
- tv-describe now renders the actionable first line of each label and
  returns it as focusedLabel, with the remaining lines shown as compact
  context, so a copied label round-trips through tv-set-focus.
- Clarify the tv-set-focus label hint: use the first line; matching is
  case-insensitive with prefix/substring fallback.

Verified end-to-end against the NFL Connected app on a tvOS simulator:
Home/Games/Teams/Settings, case-insensitive, and middle-line substrings
(e.g. a team name inside a game cell) now all resolve and move focus;
bogus labels still report ok=false.
…no-op

Gesture, keyboard, paste and rotate tools drive simulator-server over a
fire-and-forget transport (sendCommand returns void, no ack) and then
unconditionally report success. A tvOS UDID classifies as platform "ios"
by shape, so simulator-server spawned and accepted the touch/key
commands
— but Apple TV has no touchscreen, so they no-op'd while the tool still
returned { tapped: true }. Misleading.

Guard at the simulator-server factory — the one chokepoint every gesture
/
keyboard / paste / rotate tool resolves through (and screenshot-diff,
run-sequence, flows). It rejects tvOS sims with an
UnsupportedOperationError
pointing at the tv-* tools, mirroring the inverse guard tv-control
already
has. screenshot is unaffected: it branches to xcrun before resolving
this
service.

Also unwrap the cause chain in the HTTP error mapper so an
UnsupportedOperationError / NotImplementedOnPlatformError thrown inside
execute() or a factory maps to a clean 400 / 501 instead of a generic
500
(the registry wraps every throw in ToolExecutionError).
After launch-app / restart-app the foreground app sits on its splash /
loading screen with no focusable elements yet — for a React Native app,
until the JS bundle loads. A single describe in that window returns an
empty list indistinguishable from a real "AX is broken" result, so an
agent reads "(none reported)" as a dead end and gives up. Restart shows
it more than launch because the RN bundle reload takes longer.

Ride out the transition: retry describe a few times over a short window,
and when it's still empty, append a hint explaining the app is most
likely
still launching and to wait ~2-3s and retry (or screenshot to confirm).
No native change — the AX daemon already tracks the foreground app
correctly (HeadBoard → app → loaded); this is purely about not
presenting
the transient empty state as a failure.
…start-app

launch-app and restart-app declared the iOS-only native-devtools service
eagerly in their `services()` map. A tvOS simulator classifies as platform
"ios" by UDID shape, so for an Apple TV target the registry resolved that
service — and the native-devtools factory runs `simctl spawn <udid> launchctl
setenv DYLD_INSERT_LIBRARIES <ios-dylib>` at resolution time. That poisons the
tvOS sim's launchd env with an iOS-built dylib it can't load, and the precheck
could return a misleading init_failed / restart_required result. The tvOS
skills route agents straight into these tools, so this was on the happy path.

Resolve native-devtools lazily inside the iOS handler (closing over the
registry), mirroring the established `describe` / `screenshot` pattern, and skip
it entirely when `isTvOsSimulator` is true — tvOS has no native-devtools
injection. Both tools now declare no eager service, so a tvOS udid never spins
up the iOS-only blueprint. Regular iOS launch/restart behaviour is unchanged.
… init

The boot watcher resolves the iOS-only native-devtools service for every booted
simulator on each 10s poll. A tvOS sim classifies as platform "ios" by UDID
shape, so the watcher kept trying to `simctl spawn … launchctl setenv
DYLD_INSERT_LIBRARIES` an iOS-built dylib into the Apple TV sim — which it can't
load — and re-attempting it every poll.

Skip tvOS udids in `initUdid`. `isTvOsSimulator` is memoized, so re-probing a
still-untracked udid each poll is a cheap cached lookup. The Apple TV focus
state is served by the tv-control daemons, not native-devtools.
screenshot-diff was registering simulatorServer unconditionally in its
services() declaration. The registry resolves all declared services before
execute() runs, so even a call with two saved PNG paths (no live capture)
triggered a SimulatorServer startup — which fails on tvOS simulators that
have no SimulatorServer backend.

Only register the simulatorServer service when captureBaseline or
captureCurrent is true; skip it entirely for static-path diffs.
…focused as success

Two separate fixes in tv-control.ts:

1. Stale ax-service after launch-app / restart-app
   The tvos-ax-service daemon is spawned inside the simulator's process tree.
   When launch-app or restart-app kills the app, the daemon exits with it and
   its AX connection to the old process is gone. The next tv-describe call
   sent to the dead socket returned an empty focusable list, which the tool
   mis-reported as "still launching" — indefinitely.

   Fix: track axExited on the daemon's exit event. ensureAxAlive() respawns
   the daemon on demand (remove stale socket → spawnAxDaemon → waitForSocket)
   and is called at the top of describe(), hierarchy() and setFocus(). The
   HID daemon exit path is unchanged — it has no reconnect path and still
   terminates the service.

2. tv-set-focus ok:false on already-focused element
   The native setNativeFocus returns NO when the element is already focused
   (the focus engine refuses a no-op). The tool was surfacing this as
   ok:false with a misleading "AutomationEnabled may not be set" message.

   Fix: when the native call returns ok:false, call describe() to check
   whether the requested label already has focus. If it does, return
   {ok: true, message: "Already focused"} — a no-op move is a success.
…tors

Native injection on tvOS was silently failing because DYLD_INSERT_LIBRARIES
was set to the IOSSIMULATOR-platform bootstrap dylib. dyld refuses to load a
dylib whose LC_BUILD_VERSION platform doesn't match the process, so the
library was skipped and native-devtools never connected (connected:false
indefinitely even after restart-app).

The dylibs/tvos/ directory already ships the correct TVOSSIMULATOR-platform
slices — they just weren't being used.

native-devtools-ios:
- Add DYLIB_TVOS_DIR = dylibs/tvos/
- Export bootstrapDylibPathTvos() and nativeDevtoolsDylibPathTvos()
  pointing at the tvOS-specific dylib files

native-devtools blueprint:
- Import bootstrapDylibPathTvos and isTvOsSimulator
- In ensureEnv(), call isTvOsSimulator(udid) and route to
  bootstrapDylibPathTvos() for Apple TV simulators, leaving the existing
  iOS (unix/tcp) paths unchanged
The launch-app / restart-app iOS impls and simulator-watcher early-returned
on `isTvOsSimulator(...)` before resolving the native-devtools service, on the
assumption that the iOS-built dylib could not load into an Apple TV process.
That left injection permanently un-attempted on tvOS (connected:false forever).

The native-devtools blueprint's ensureEnv now selects the platform-matched
DYLD_INSERT_LIBRARIES slice (the TVOSSIMULATOR bootstrap for Apple TV sims), so
resolving the service injects correctly on both iOS and tvOS. Remove the three
obsolete skip-guards so tvOS resolves native-devtools too, and bump
argent-private to the build carrying the tvOS daemon/dylib slices.

Tests updated: launch-restart-tvos now asserts tvOS DOES resolve native-devtools;
native-devtools-factory-cleanup mocks isTvOsSimulator->false to avoid the
simctl-list probe hang.
A tvOS UDID classifies as platform "ios" by shape, but the SimulatorServer
blueprint throws UnsupportedOperationError on start (it can't drive the focus
engine). A start that throws leaves the registry node in ERROR state, not IDLE.
Both stop tools treated "any non-IDLE node" as a running server, so they
reported stopped:true for a server that never ran — misleading on tvOS, where
every gesture/keyboard/rotate call leaves such an ERROR node behind.

Add isLiveServiceState() (RUNNING|STARTING only) to the registry. The stop
tools still dispose non-IDLE nodes (cleaning up dead ERROR nodes) but only
report stopped:true / include the URN when the node was actually live.

Verified live on an Apple TV sim: stop-simulator-server -> stopped:false, even
after a gesture-tap creates the ERROR node; stop-all omits the ERROR node.
The ax daemon is a standalone process spawned via `simctl spawn`, not a
child of the app, so it survives launch-app / restart-app. The prior
respawn path was gated on the daemon's exit event (dc3556d), which never
fires for the real failure mode: AXRuntime's `primaryApp` cache inside the
still-running daemon goes stale and points at the killed app, so describe
returns 0 focusable elements on a fully-rendered screen with a misleading
"still launching" hint. The only recovery was a manual `pkill`.

- Add `recycleAx()` to TvControlApi: force-kill + clear socket + respawn +
  wait-for-accept (the programmatic equivalent of pkill). Extract the shared
  `spawnFreshAx()` used by both `ensureAxAlive()` (exit-path) and
  `recycleAx()`; coalesce concurrent callers onto a single respawn.
- tv-describe: after the transition-retry window is still empty, recycle the
  daemon once and re-probe. A fresh daemon rebinds to the current foreground
  app, so a stale cache now populates while a genuinely-loading screen stays
  empty. Update EMPTY_HINT to reflect that recycle was already attempted.
- Tests: add recycleAx to the mock, a stale-cache-recovery test, and update
  the exhaustion test.

Verified on an Apple TV sim: killing the in-sim ax daemon now recovers
transparently on the next tv-describe (PID changes, correct focus data, no
manual pkill).
…ently no-op

The tvos-hid-daemon runs on the host and holds a SimDeviceLegacyClient bound
to a specific sim boot for its whole lifetime. A reboot (Shutdown→Booted or a
force reboot) invalidates that client but leaves the daemon process alive, and
its navigate/type sends are fire-and-forget — so tv-navigate returns {sent}
while the screen never moves, with no error and no recovery (the daemon-exit
reconnect never fires). The ax-service does NOT have this problem: it runs
inside the sim via simctl spawn, so the reboot kills it and the next describe
respawns it (546fe0a).

boot-device now disposes the cached TvControl:<udid> service on a tvOS boot
transition, so the next tv-* call rebuilds it with a fresh daemon bound to the
new boot. Disposal is gated on needsPreBoot (the exact reboot transitions) and
tvOS runtimeKind; ServiceNotFoundError (nothing cached — the fresh-boot case)
is swallowed.

- boot-device.ts: capture runtimeKind from listIosSimulators; dispose
  TvControl:<udid> after bootstatus when isTvOs && needsPreBoot.
- boot-device.test.ts: +4 tests (Shutdown boot disposes, force reboot
  disposes, non-tv boot does not, ServiceNotFoundError swallowed).
build.sh built the two tvOS daemon binaries but never the three tvOS
injection dylibs that bootstrapDylibPathTvos() requires. They existed
only as hand-built, gitignored artifacts, so a clean checkout shipped a
tool-server that threw "dylib not found" and failed launch-app /
restart-app on Apple TV.

Compile the same sources as the iOS dylibs (InjectionEntry + ViewHierarchy,
KeyboardPatch, InjectionBootstrap) against the appletvsimulator SDK into
dylibs/tvos/ as a universal arm64+x86_64 slice. All three are required
because InjectionBootstrap dlopen()s the other two from its own directory.
Add PREBUILT_*_TVOS hooks (including on the prebuilt early-exit path) and
release codesigning, mirroring the iOS build.
The launch-app / restart-app header comments and tool descriptions claimed
the iOS handler skips native-devtools injection entirely on tvOS. That was
true when written, but became stale once native-devtools learned to inject
the platform-matched TVOSSIMULATOR slice — injection now runs on tvOS too
(and the tvOS dylibs are required, see the build fix). Rewrite both to match
the accurate platforms/ios.ts comment and the launch-restart-tvos test.

Also remove the unused execFileAsync binding (and its now-orphaned promisify
import) from tv-control.ts; execFile itself is still used by the daemon
spawn helpers.
Android TV (leanback) is plain Android driven by adb, so it reuses the
existing tv-* tool surface rather than a parallel toolset. The tvOS
TvControlApi contract is extracted to tv-control-types.ts and implemented
a second time, adb-backed, in android-tv-control.ts:

- navigate → input keyevent (DPAD/BACK/HOME/MEDIA_PLAY_PAUSE)
- describe → uiautomator dump projected to the focus contract
- type     → input text
- set-focus → bounded D-pad walk toward the target (no native jump)

Discovery: AndroidDevice gains runtimeKind, detected via
`getprop ro.build.characteristics` containing `tv`, with cached
getAndroidRuntimeKind / isAndroidTv helpers paralleling the iOS side.
list-devices surfaces it so a TV target is identified by runtimeKind:"tv"
across both platforms.

launch-app / restart-app resolve LEANBACK_LAUNCHER on TV targets (with a
LAUNCHER fallback); describe attaches a tv-* hint on Android TV. The tv-*
tools dispatch to the Apple or Android backend by platform via tvServiceRef.

Docs (tvos skills + rules) broadened to cover both Apple TV and Android TV.
…racteristics

Live testing against the Google ATV emulator (Television_1080p AVD) showed it
reports ro.build.characteristics=emulator (no `tv` token), so the original
characteristics-only check classified every TV emulator as a phone — and
list-devices tagged it runtimeKind:"mobile", which would route the tv-* tools
to the wrong backend.

Make `pm list features` (android.software.leanback /
android.hardware.type.television — exactly what PackageManager.hasSystemFeature
checks) the primary signal, keeping the characteristics `tv` token as a
secondary fallback for images where the feature list is unavailable. Verified
live: the TV emulator now reports runtimeKind:"tv" and a phone emulator
runtimeKind:"mobile".
…d TV

The two TV skills now cover both Apple TV and Android TV, so rename them off
the tvos-centric names and fold in the findings from an end-to-end Android TV
test run against the real app.

Renames (via git mv, history preserved):
  argent-tvos-interact        -> argent-tv-interact
  argent-tvos-simulator-setup -> argent-tv-setup   (also drops "simulator",
                                 since Android TV is an emulator)
Updated the frontmatter `name` in both and every reference: rules/argent.md,
argent-android-emulator-setup, argent-device-interact, and the two user-facing
hint strings in tool-server (describe ios platform + simulator-server
unsupported-op error). argent-tv-setup is restructured with parallel
Apple-TV / Android-TV setup paths instead of Android-as-an-afterthought.

Live-testing findings folded in:
- argent-tv-interact: some react-native-tvos screens report focusableCount:0
  because they use RN's own focus engine (invisible to the Android
  accessibility tree) — faithful, not a bug; use screenshot + the full
  `describe` tool and drive blind with tv-navigate. Added the post-launch
  empty-focus (bundle still loading) troubleshooting row, and noted that
  tv-type reports success even when the field never gained focus (verify with
  a follow-up describe/screenshot).
- argent-android-emulator-setup: point leanback AVDs at the tv-* tools (runtimeKind:"tv" is detected via the system feature list, not the serial).
- argent-device-interact: correct "do not work on TV" — on Android TV the
  gesture/button/keyboard tools technically execute but are the wrong tool;
  on Apple TV they're blocked.
A tvOS sim reboot (Shutdown→Booted, or a force reboot) wipes launchd's
DYLD_INSERT_LIBRARIES, but the cached NativeDevtools service keeps a sticky
`envSetup=true` from the previous boot — so `ensureEnvReady()` short-circuits
and never re-injects. Result: launch-app / restart-app produce an uninjected
process and native-devtools-status stays `connected:false` until the
tool-server restarts.

The simulator-watcher disposes NativeDevtools on shutdown, but it polls every
10s; a fast `boot-device force:true` can complete Shutdown→Booted between two
polls, so the watcher never observes the transition. Dispose the cached service
synchronously in boot-device (alongside the existing TvControl recycle) so the
following resolveService rebuilds it with a fresh `envSetup=false` and ensureEnv
re-applies DYLD on the new boot. Gated to tvOS to match the validated repro and
leave the well-exercised iOS boot path untouched.
Binary adb execs (runAdbBinary, encoding:"buffer") reject with Buffer
stderr/stdout, but describeAdbFailure called `(e.stderr ?? "").trim()`
on a `stderr?: string` type — Buffer has no `.trim`, so the handler
itself threw `(e.stderr ?? "").trim is not a function`, masking adb's
real diagnostic. Hit live on tv-describe when `uiautomator dump` fails
during a loading/transition window; affects any binary-exec failure
(screencap too), not just Android TV.

Widen the local type to string | Buffer and coerce both fields via
toString() before trimming so the handler surfaces the actionable adb
error instead of crashing.
…element as success

The native ax daemon's tiered matcher can resolve a setfocus query via an
annotation substring (e.g. "Lander" hitting the compound label
"Home\n…Lander…"). When that element is already focused, setNativeFocus
returns NO and the host-side fallback compared the focused first line against
the raw query — which never matched — so it surfaced the misleading
"setNativeFocus returned NO (AutomationEnabled may not be set)" instead of
"Already focused". Compare against the matcher's echoed matchedLabel, falling
back to the raw query when absent so exact-name calls are unchanged.
…lookup

The component-source AST indexer's variable_declarator branch only matched
a value node that was directly an arrow_function/function_expression, so
every component declared via an HOC wrapper —
`export const X = React.memo(...)` / `forwardRef(...)` — produced a
call_expression value node and was silently dropped from the index.
`react-profiler-component-source` then returned `found: false` for exactly
the components a profiling session surfaces (8 such components in the NFL
Connected app alone: EnhancedView, BannerFactory, ResizableIcon, …).

Unwrap the memo/forwardRef HOC chain (bare and React.-qualified, including
nesting and the `forwardRef<T, U>(…)` generic form) before classifying the
value as a component, anchoring the index entry to the `const` line. memo
wrapping now also feeds isMemoized directly.

Adds ast-index-hoc.test.ts covering each declaration shape plus a
non-component call negative case.
scianek added 2 commits June 17, 2026 17:49
…ock, deps)

Post-rebase fixes so the TV work is green on top of origin/main (0.12.0):

- boot-device.test.ts: main's bootIos (#346) now calls ndApi.reverifyEnv();
  add reverifyEnv to the NativeDevtools mock in the 9 TV-added tests that
  only stubbed getInitFailure (they were written before that call existed).
- tv-control.ts: satisfy main's new ESLint gate (#349) — _code for the
  unused exit arg, comments in two intentional best-effort empty catches.
- launch-app/index.ts, restart-app/index.ts: drop now-unused *Params type
  imports left over from the platform-dispatch refactor.
- eslint.config.mjs: ignore the downloaded Perfetto trace-processor bundle
  (git-ignored generated artifact), matching the other assets/ ignores.
- package-lock.json: reconcile after the merged dependency set.
Format-only pass to satisfy the prettier --check CI gate (18 files: markdown
table padding / emphasis, import re-wrapping, ternary line-joining). No logic
changes — `git diff -w` is cosmetic-only and all 1345 tool-server tests pass.
…package

Consumer side of the tvOS distribution fix. The publish pipeline downloads
prebuilt signed binaries from argent-private-releases rather than building
from source; the tvOS binaries were neither downloaded nor verified, so a
published @swmansion/argent shipped without them and TV support failed at
runtime (local pack:mcp:local works only because it builds from the submodule).

- download-native-binaries.sh: download tvos-ax-service and tvos-hid-daemon
  into bin/darwin/, and extract the tvOS dylib tarball into dylibs/tvos/ (the
  dir bootstrapDylibPathTvos reads). Added all three to signature verification.
- publish.yml / build-package-artifact.yml: add `test -f` checks for the five
  tvOS artifacts so a missing binary fails the build loudly instead of
  silently shipping a package where the tv-* tools crash. Mirrors the existing
  iOS ax-service / dylib checks.

Did NOT flip bundle-tools.cjs to required:true for the tvOS binaries: the iOS
ax-service and dylibs are also required:false there, with enforcement living
in these workflow checks. Matching that precedent keeps local source builds
from hard-failing and avoids a release-ordering trap.

Depends on the argent-private workflow change that publishes these binaries to
the release; until that lands and a release is cut, these download/verify
steps have nothing to fetch.

@filip131311 filip131311 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tools like tv-describe should realy just be "describe" and use our platform fork mechanism:

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants