feat(tv): Apple TV (tvOS) and Android TV support#374
Open
scianek wants to merge 33 commits into
Open
Conversation
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.
…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
requested changes
Jun 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 texttv-navigate— send a remote direction / select / menu / home / playpausetv-set-focus— jump focus to an element by accessibility labeltv-type— type into the focused fieldApple TV runs two native daemons (in-sim AX service + host-side HID daemon), shipped via the
argent-privatesubmodule. Android TV reuses the same tool surface but is adb-backed (input keyevent,uiautomator dump,input text).Key design points
runtimeKinddetection (tvOS runtime string for Apple TV;pm list featuresleanback/television for Android TV — notro.build.characteristics, which lies on TV emulators).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.Dependency / merge order
Pins the
argent-privatesubmodule to the tip of itsfeat/tv-supportbranch (software-mansion/argent-private#20). Merge that PR tomainfirst, then bump the submodule pointer here to the merged SHA before merging this PR.🤖 Generated with Claude Code