Skip to content

feat(tool-server): add system-settings tool for device-wide display, accessibility and radio settings - #566

Draft
latekvo wants to merge 7 commits into
mainfrom
feat/system-settings-tool
Draft

feat(tool-server): add system-settings tool for device-wide display, accessibility and radio settings#566
latekvo wants to merge 7 commits into
mainfrom
feat/system-settings-tool

Conversation

@latekvo

@latekvo latekvo commented Jul 24, 2026

Copy link
Copy Markdown
Member

What

A new cross-platform system-settings tool, so the agent can change device-wide display, accessibility and radio settings without navigating the system Settings UI. It belongs to the same "system Settings, out of band" family as settings-permissions.

Each setting maps to one platform command. appearance takes light | dark, text-size takes one of the 12 Dynamic Type categories, and every other setting takes on | off:

setting iOS simulator Android
appearance simctl ui appearance light|dark cmd uimode night no|yes
text-size simctl ui content_size <category> settings put system font_scale <float>
increase-contrast simctl ui increase_contrast enabled|disabled settings put secure high_text_contrast_enabled 0|1
reduce-motion defaults write ReduceMotionEnabled the three *_animation_scale globals
invert-colors defaults write ClassicInvertColorsEnabled settings put secure accessibility_display_inversion_enabled
wifi - svc wifi enable|disable
cellular - svc data enable|disable
airplane-mode - cmd connectivity airplane-mode enable|disable
location - settings put secure location_mode 3|0
auto-rotate - settings put system accelerometer_rotation 1|0

Returns { setting, value, applied }, where applied is the concrete platform-level change: appearance=dark, night_mode=yes, font_scale=1.94, ReduceMotionEnabled=YES, and so on.

Notes on the design

  • Flat { udid, setting, value } schema, like tv-remote. The registry's zod-to-JSON-schema helper is typed for z.ZodObject, so a top-level discriminated union isn't an option; value is validated against the setting before dispatch instead.
  • iOS simulators and Android only. simctl ui is simulator-only, so there's no apple.device support; the adb commands work on emulators and real devices alike. The five radio / location / rotation settings are Android-only and the iOS handler rejects them as caller input with the list of what iOS does support.
  • No appleRemote, unlike settings-permissions. sim-remote forwards a fixed set of simctl verbs and ui is not among them, so three of the five iOS settings have no remote path at all - declaring the capability would accept appearance on a remote sim and then fail inside the handler.
  • An out-of-range value is rejected up front as InvalidToolInputError, which the HTTP layer maps to 400, matching how the keyboard backends reject un-typeable characters. It carries the granular SYSTEM_SETTING_UNSUPPORTED telemetry code.
  • simctl ui refusals are read off stderr, not the exit code. content_size and increase_contrast print Invalid argument and still exit 0 - only appearance also exits non-zero - so an exit-code-only check would answer applied for a setting the runtime never changed. The defaults path keeps using the exit code, because simctl spawn propagates it faithfully.
  • Three new failure codes mirroring the settings-permissions family: SYSTEM_SETTING_UNSUPPORTED, IOS_SYSTEM_SETTING_FAILED, ANDROID_SYSTEM_SETTING_FAILED.

Verification

  • 36 unit tests covering the exact command args per setting and platform, the full font-scale map, the stderr-refusal path, both boot-hint wordings, error classification and the 400 input-validation path. Full tool-server suite green (3106 passed, 296 files); tsc including the tsconfig.test.json gate, prettier and eslint clean.
  • Android 14 / API 34 emulator, every one of the ten settings run over adb with an independent read-back, then restored to baseline: font_scale, high_text_contrast_enabled, accessibility_display_inversion_enabled, accelerometer_rotation and the three animation scales via settings get; cmd uimode night and cmd connectivity airplane-mode via their own query form; location_mode cross-checked with cmd location is-location-enabled (false after off); svc wifi and svc data via global wifi_on / global mobile_data. Failure paths confirmed non-zero (settings put system font_scale not-a-number → 255, cmd uimode night wat → 255), which is what runAdb turns into a throw.
  • iOS 18.6 simulator: all 16 valid (option, value) pairs the tool can emit exit 0 with empty stderr, so the stderr guard cannot false-positive; every rejected argument for content_size / increase_contrast exits 0 with stderr set. The defaults writes read back through simctl spawn defaults read. Shut-down-simulator wording captured for both mechanisms: simctl ui answers Unable to lookup in current state: Shutdown, simctl spawn answers Process spawn via launchd failed because device is not booted - both now carry the boot-device hint.
  • End to end through the real HTTP entry point (worktree tool-server, POST /tools/system-settings) on both platforms, each change confirmed by read-back and a screenshot, then round-tripped back to defaults. An invalid value returns 400 with the list of valid values.

latekvo and others added 5 commits July 24, 2026 13:59
…trast, and text size

Adds a cross-platform `system-settings` tool that changes device-wide display
and accessibility settings directly, without navigating the Settings UI:

- appearance: light / dark theme
- increase-contrast: accessibility high-contrast
- text-size: the 12 Dynamic Type categories (extra-small … accessibility-xxxl)

iOS simulators go through `simctl ui` (appearance / increase_contrast /
content_size); Android emulators and devices through `adb cmd uimode night`,
the high_text_contrast_enabled flag, and font_scale (each iOS text category
maps to the nearest scale). An out-of-range value is rejected up front as a
400 input error carrying the SYSTEM_SETTING_UNSUPPORTED telemetry code.
…y, a11y, and Android device state

Adds reduce-motion and invert-colors (both platforms) plus the Android-only
wifi, cellular, airplane-mode, location, and auto-rotate toggles alongside the
existing appearance / text-size / increase-contrast. iOS reaches reduce-motion
and invert-colors through the com.apple.Accessibility defaults domain (posting
the change notification so running apps re-read live) and rejects the
Android-only settings as invalid input. Unifies every boolean toggle on on/off.
…ings

Main's interaction-messages test requires every registered tool to define
started/completed/failed message formatters; add them for system-settings and
bump the expected tool count to 78 for the newly added tool.
`simctl ui <udid> content_size <value>` and `increase_contrast <value>`
print "Invalid argument" on stderr and still exit 0 — only `appearance`
also exits non-zero. Reading the exit code alone reported `applied` for a
setting that never changed, which is exactly what a runtime that does not
model the option produces. Read stderr and fail on it.

The boot-device hint matched only `simctl ui`'s "current state: Shutdown",
so `reduce-motion` and `invert-colors` — the two settings that go through
`simctl spawn`, which says "device is not booted" — lost the hint their
own comment promised. Match both wordings.

Verified on iOS 18.6 (39B9C547): all 16 valid (option, value) pairs exit 0
with empty stderr, so the guard cannot false-positive; every rejected
argument for content_size/increase_contrast exits 0 with stderr set.
`simctl spawn` propagates exit codes faithfully (42 -> 42), so the defaults
path keeps using the exit code.
@latekvo latekvo changed the title feat(tool-server): add system-settings tool to toggle appearance, contrast, and text size feat(tool-server): add system-settings tool for device-wide display, accessibility and radio settings Jul 30, 2026
latekvo and others added 2 commits July 30, 2026 19:10
Setting `invert-colors` on and then reading a screenshot back is the
obvious way to confirm it, and it always looks like the change failed.
Verified on both platforms: with the setting reading back as on, the
capture is byte-identical to the un-inverted one (Android 14 `screencap`,
which composites with an identity color matrix, and iOS 18.6 `simctl io
screenshot`).
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.

1 participant